I'm writing a multi-platform app in SwiftUI.
It handles 3 different kinds of windows, and as many instances of each as the user wants to open.
I need to do some cleanup when the user closes a window, so I added the following to the top-level View in my View hierarchy (a NavigationStack
in this case)
.onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in
// Do cleanup
}
My expectation is that the closure for my .onReceive
handler would be called when the specific window that contains this specific View object is closed.
If you look up the NSWindow.willCloseNotification
in AppKit, it says that it sends the notification to the specific window object that is about to be closed.
However, it seems that in SwiftUI, it gets called on every window when any window is about to be closed.
Why is that, and is there some trick to getting a notification when a specific instance of NSWindow is about to be closed?
If you look up the
NSWindow.willCloseNotification
in AppKit, it says that it sends the notification to the specific window object that is about to be closed.
I believe that there might be a misunderstanding here of what the documentation is trying to express. From the docs:
The notification object is the
NSWindow
object that’s about to close. This notification doesn’t contain auserInfo
dictionary.
"The notification object" here refers to Notification.object
, which is a generic property (general, not in the <T>
sense) that describes an object which the notification is relevant to. Some notifications are global and not related to a specific object (in which case it can be nil
), while others are indicating a change relevant to a specific object, in which case this will be set.
In this specific case, the object
property of a Notification
received for NSWindow.willCloseNotification
is documented to be the window which will be closing, so that you can distinguish between windows which can close.
Notification.object
isn't just relevant information when receiving notifications, though: you can use a specific object as a filter for notifications to receive when subscribing to notifications. Both NotificationCenter.addObserver(forName:object:queue:using:)
and NotificationCenter.addObserver(_:selector:name:object:)
allow you to pass an object
parameter in such that only notifications whose .object
matches the given object are delivered — i.e., in case you only care about receiving NSWindow.willCloseNotification
for a specific window object, you can pass it in up-front and notifications will only be delivered when .object
matches.
Crucially, NotificationCenter.publisher(for:object:)
also allows you to pass in an object
to filter for — it just happens to default to nil
(which means that no filter is applied). The net effect of subscribing for notifications with no object is that each window which subscribes will receive notifications for any window that closes; i.e., exactly what you're seeing.
If you want each view hierarchy to be informed only when its containing window is about to close, you'll need to provide a reference to the containing NSWindow
to publisher(for:object:)
:
NSHostingController
/NSHostingView
, the easiest way to do this would be to grab a reference to the presenting window at NSHosting*
creation time, then pass that in to your SwiftUI hierarchyApp
lifecycle then accessing the AppKit world is likely easiest through NSViewRepresentable
— it's possible to inject an empty NSView
into your hierarchy whose sole purpose is to provide a reference to its NSWindow
: https://stackoverflow.com/a/63439982/169394