Как правильно закрыть поповер?

В моем подклассе NSDocument я создаю экземпляр NSPopover с поведением .semitransient и показываю его:

popover.show(relativeTo: rect, of: sender, preferredEdge: .maxX)

popover объявляется локально. Метод кнопки в контроллере popover вызывает:

view.window?.close()

Всплывающее окно закрывается, но я заметил, что оно остается в памяти, deinit() никогда не вызывается, а счетчик NSApp.windows увеличивается, тогда как если я закрываю его, нажимая escape или щелкая вне его, вызывается deinit и количество окон не увеличивается.

Если я установлю .isReleasedWhenClosed окна на true, количество окон не увеличится, но deinit все равно не будет вызван.

(Свифт 3, Xcode 8)


person AlexT    schedule 24.02.2017    source источник


Ответы (3)


Вы должны вызвать performClose (или close) во всплывающем окне, а не в окне.

person DrummerB    schedule 24.02.2017
comment
Мы находимся в подклассе NSViewController, у которого нет этого метода. Как и его мнение. Окно представляет собой NSPopoverWindow, для которого я не могу найти документацию. - person AlexT; 24.02.2017
comment
Где-то вы объявили ту переменную popover, для которой вы вызываете showRelativeTo:of:preferredEdge:. Это объект, для которого вам нужно вызвать performClose. Если вам нужно закрыть его из кода внутри NSViewController, встроенного во всплывающее окно, вам нужно найти способ получить доступ к этому объекту. Возможно, создайте PopoverViewController в качестве базового класса со свойством popover, которое вы установили перед его отображением. Или используйте шаблон делегата. Или используйте уведомления (предпочитайте другие решения, так как это очень быстро приведет к спагетти-коду). - person DrummerB; 24.02.2017
comment
Я только что попробовал эту первую идею, добавив ссылку на всплывающее окно и инициализировав его перед вызовом show(...). Затем в «завершенном» методе, вызывающем popover.close(). Он закрывается нормально, но никогда не деинитируется. - person AlexT; 24.02.2017
comment
@AlexT Как вы объявили popover? Он не будет deinit, если вы держите сильную ссылку на него. - person DrummerB; 24.02.2017
comment
Объявлено локально. Я добавил кнопку «Отмена» без прикрепленного кода. Всплывающее окно исчезает, но deinit не вызывается, и, как ни странно, окно больше не находится в массиве NSApp.windows, поэтому оно висит там в памяти без ссылок. - person AlexT; 25.02.2017
comment
Кажется, была некоторая несогласованность в зависимости от того, закрыл ли я всплывающее окно клавишей escape или вызвал view.window!.close() . Поэтому я сфабриковал событие escape keydown и опубликовал его вместо этого. Не большая разница. Иногда deinit срабатывает через несколько секунд после закрытия всплывающего окна; это что-то вроде сбора мусора на работе? Для тех, кому интересно (потому что это трудно обнаружить), управляющая клавиша — \u{1B}, а ее код — 53. - person AlexT; 25.02.2017
comment
Я только что обнаружил, что существует протокол NSPopoverDelegate, который может принять ваш ViewController, чтобы получать такие уведомления, как willShow, didClose и т. д. Назначьте его перед вызовом popover.show(). Однако с этой проблемой мало пользы. - person AlexT; 25.02.2017
comment
Еще один комментарий! Мои всплывающие окна прикреплены к документам в приложении на основе документов. Иногда их можно поднимать десяток и более раз. Когда я закрываю документ, все дейниты сгорают вместе. Похоже, что они каким-то образом принадлежат документу, хотя я не сохраняю ссылки на них после того, как покажу их. - person AlexT; 26.02.2017
comment
@AlexT Если вы опубликуете простой проект, демонстрирующий проблему, я могу взглянуть на него. - person DrummerB; 26.02.2017

Спасибо -DrummerB за проявленный интерес. Мне потребовалось некоторое время, чтобы создать простое тестовое приложение, которое я мог бы отправить вам, и, конечно же, оно не было основано на документах, как мое, и это, казалось, затуманивало проблему. Мой способ открытия всплывающего окна был основан на примере, который я недавно прочитал, но сейчас не могу найти или предупредить людей. Это было так:

let popover = NSPopover
let controller = MyPopover(...)! // my convenience init for NSViewController descendant
popover.controller = controller
popover.behaviour = .semitransient // and setting other properties
popover.show(relativeTo: rect, of: sender, preferredEdge: .maxX)

Вот улучшенный способ, с которым я столкнулся:

let controller = MyPopover(...)! // descendant of NSViewController
controller.presentViewController(controller, 
    asPopoverRelativeTo: rect, of: sender, preferredEdge: .maxX,
    behavior: .semitransient) // sender was a NSTable

В контроллере представления действие кнопки «Готово» просто выполняет:

dismissViewController(self)

который раньше никогда не работал. И теперь я обнаружил, что список окон приложения не растет, и deinit контроллера происходит надежно.

person AlexT    schedule 05.03.2017
comment
Вернувшись к этому через пару недель по другим проблемам; этот ответ не совсем правильный. deinit происходит правильно, но список окон, как сообщает NSApp.windows, продолжает расти, но не в том случае, если вы использовали клавишу escape, чтобы закрыть всплывающее окно. Помогите, пожалуйста! - person AlexT; 12.03.2017

Я бы предложил сделать следующее:

Определите такой протокол

protocol PopoverManager {
    func dismissPopover(_ sender: Any)
}

В вашем popoverViewController (в этом примере мы отображаем контроллер представления фильтра как всплывающее окно) добавьте переменную для popoverManager, как это

/// Filter shown as a NSPopover()
class FilterViewController: NSViewController {
    
    // Delegate
    var popoverManager: PopoverManager?
        
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do view setup here.
        
    }
    
    // Bind this to the close button or action on your popover view controller
    @IBAction func closeAction(_ sender: Any) {
        self.popoverManager?.dismissPopover(sender)
    }

   ...

}

Теперь в вашем viewController вы показываете всплывающее окно, добавляя расширение, подобное этому.

extension MainViewController: NSPopoverDelegate, PopoverManager {
    @IBAction func setFilter(_ sender: AnyObject) {
        self.showFilterPopover(sender)
    }
    
    func showFilterPopover(_ sender: AnyObject) {
        let storyboard = NSStoryboard(name: "Filter", bundle: nil)
        guard let controller = storyboard.instantiateController(withIdentifier: "FilterViewController") as? FilterViewController else {
            return
        }
        // Set the delegate to self so we can dismiss the popover from the popover view controller itself.
        controller.popoverManager = self
        
        self.popover = NSPopover()
        self.popover.delegate = self
        self.popover.contentViewController = controller
        self.popover.contentSize = controller.view.frame.size
        
        self.popover.behavior = .applicationDefined
        self.popover.animates = true
        self.popover.show(relativeTo: sender.bounds, of: sender as! NSView, preferredEdge: NSRectEdge.maxY)
        
    }
    
    func dismissPopover(_ sender: Any) {
        self.popover?.performClose(sender)
        // If you don't want to reuse it
        self.popover = nil
    }
}
person Duncan Groenewald    schedule 09.03.2021