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 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 an 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.
applicationDidUpdate(_:)
is called after the update. I use this to cache the known windows of a given update cycle.applicationWillUpdate(_:)
is called before all windows will be updated with whatever an update does, including newly opened windows. Here, I compare the known windows with the cache to see if any window was opened or closed between cycles.
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.
Updated 2022-11-16: Fixed typos, thanks Marin!