React to NSWindow Showing in Your App

How do you observe for changes to the list of visible/known/active NSWindow instances in your app?

There's NSWindow.willCloseNotification, but you there's no equivalent like a willShowNotification or didShowNotification (except for NSPopover). I don't know why, but I do find it strange.

Given that you have a NSWindowController subclass for every window in your app, you could just post your own notification in showWindow(_:). But in my app, the WordCounter, there are like 4 different places windows are created. I cannot make all inherit from a common controller base (most implementations are hidden inside framework projects). So what other options do we have?

I considered trying KVO on the NSApp.windows collection – but in the process discovered that the NSApplicationDelegate has two methods I've never used before in my whole life: applicationWillUpdate(_:) and applicationDidUpdate(_:). Both are called all the time to propagate events to windows. But I found one can use these to compute a diff of known windows.

In the WordCounter, I specifically want to react to "any window open" and "all windows closed" events. So I do not need to know which window was opened or closed, only the aggregate of currently opened or closed windows. I am leaving the array diffing based on object identity as an exercise to the reader.

This is the code I use to figure out when the first window is shown:

class AppDelegate: NSObject, NSApplicationDelegate {

    private var windowsAfterLastUpdate: [NSWindow] = []

    func applicationDidUpdate(_ notification: Notification) {
        self.windowsAfterLastUpdate = visibleRegularWindows()
    }

    func applicationWillUpdate(_ notification: Notification) {
        let currentWindows = visibleRegularWindows()
        guard currentWindows != self.windowsAfterLastUpdate else { return }

        switch (windowsAfterLastUpdate.isEmpty, currentWindows.isEmpty) {
        case (true, false): 
            // First visible window
            break

        case (false, false):
            // New window added to the visible window list
            break

        case (false, true): 
            // All windows are now closed
            break

        case (true, true):
            // No window visible before or after the change (should not happen)
            break
        }
    }

    private func visibleRegularWindows() -> [NSWindow] {
        // You may want to adjust the filters for this. The WordCounter has a status bar item
        // and uses popovers, so I'm happy ignoring these.
        return NSApp.windows
            .filter { false == ["NSStatusBarWindow", "_NSPopoverWindow"].contains($0.className) }
            .filter { $0.isVisible }
    }

You will notice the timing is a bit off: if a window is added, it already is visible on screen, and then the change event is observed. This is fine for my case, but keep in mind that this is not a great time to prepare the window for anything. Prefer NSWindowController.loadWindow or NSWindowController.awakeFromNib or windowDidLoad or windowWillLoad for changes to a window.

I was looking for a way to subscribe to the effect of showWindow(_:), and this is a good-enough solution in my book. Hope it helps y'all when you look for a similar callback.

Browse the blog archive