Search code examples
swiftmacosswiftuinsnotificationcenter

.onReceive NSWindow.willCloseNotification called for every window in App?


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?


Solution

  • 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 a userInfo 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:):

    • If you're presenting SwiftUI using 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 hierarchy
    • If you're using the SwiftUI App 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