How to detect closing of window?
Asked Answered
S

5

9

Aim:

  • I have a swiftUI app that uses Window scene
  • When the user closes the red window, I would like call f1()

My attempt:

onDisappear doesn't seem to be called when user closes the macOS app window

Question:

  1. In macOS (SwiftUI) how do I detect a window is closed by the user?
  2. Am I missing something?

Code

App

@main
struct DemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        
        Window("test", id: "test") {
            Color.red
                .onDisappear {
                    print("onDisappear")
                    f1()
                }
        }
    }
    
    func f1() {
        print("f1 called")
    }
}

ContentView

struct ContentView: View {
    @Environment(\.openWindow) private var openWindow
    var body: some View {
        Button("show red") {
            openWindow(id: "test")
        }
    }
}
Stayathome answered 22/2, 2023 at 12:15 Comment(0)
O
7

For a "SwiftUI way" to do this, check out the environment property controlActiveState. This value will be set to .inactive when the window loses focus, and when the window is closed.

struct ContentView: View {
    @Environment(\.controlActiveState) private var controlActiveState

    var body: some View {
        Text("Hello, World!")
            .onChange(of: controlActiveState) { newValue in
                switch newValue {
                case .key, .active:
                    break
                case .inactive:
                    // Do your stuff.
                @unknown default:
                    break
                }
            }
    }
}
Obliquity answered 21/4, 2023 at 10:12 Comment(3)
Going "inactive" does not mean the window is closed ... so the question still is not answered, how to distinguish between CLOSED and INACTIVE ???Bodycheck
This does not distinguish between the window losing focus, and being closed.Masse
As others have said, window going inactive will not work. You get a .onChange() to .inactive if your app gets swapped to the background, or (in macOS) if the user brings another window to the front.Gilmagilman
G
7

In case anyone will find this interesting I have approached the problem in a different (potentially better) way.

@main
struct DemoApp: App {
    var body: some Scene {
        Window("test", id: "test") {
            Color.red
                .task {
                    // perform action when window is opened
                    await Task.waitTillCancel()
                    // perform action after window is closed
                }
        }
    }
}

extension Task where Success == Void, Failure == Never {
    static func waitTillCancel() async {
        let asyncStream = AsyncStream<Int> { _ in }
        for await _ in asyncStream { }
    }
}

According to Apple documentation, task(priority:_:) modifier creates a task that matches lifetime of the view and is cancelled when view is discarded. We can create an infinite AsyncStream (empty but never finished) to wait until task is cancelled (hence window is closed in example above) and then perform the required action.

I think this is the only properly documented case in SwiftUI when we can certainly detect when window is closed.

Gusher answered 15/10, 2023 at 18:9 Comment(2)
Great solution. This major feature should have been included in Apple's APIs.Pepys
I like this one very muchBetz
C
6

You can use the NSWindow.willCloseNotification notification:

struct ContentView: View {
    
    var body: some View {
        
        Text("xyz")
            .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in
                print("close")
            }
    }
}
Cassis answered 22/2, 2023 at 12:24 Comment(3)
Thanks a ton, just wondering if there was a SwiftUI way to do this? I was trying scenePhase, isFocused but couldn't find a wayStayathome
Important to note: this will get called when any of the app's windows are closed! newValue.object will identify which window it is, but I'm not sure how you can confirm that's the window you're interested in with SwiftUI.Sounder
@Sounder newValue.object is a instance of NSWindow. And you can identify the window using its identifier property. Another thing you might wanna know is that willCloseNotification is also posted when keyboard language has been changed. So, you should identify it properly to avoid unexpected behavior from global users.Rijeka
B
1

On Swift 5.10 Xcode 15.3 onDismiss seems to be working properly, I guess it was just a bug on the implementation.

onDismiss is called when closing a Window (manually or programatically).

Example:

WindowGroup {
    ContentView()
        .onDisappear {
            // window was closed
        }
}
Berryberryhill answered 25/3 at 17:9 Comment(2)
Doesn't work for me. Xcode 15.3, visionOS 1.1Pelion
@Pelion Sorry, I haven't tried on visionOS, but this question was specifically for macOS.Berryberryhill
C
0

From my testing using Xcode 16.0 beta on macOS 15.0 beta:

onDisappear works as expected, unless you assign a NSWindowDelegate to the window—then it is no longer called.

In that case, you have to rely on other solutions, e.g. the willClose notification (as was proposed earlier, but here is a more complete code snippet):

.onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { notification in
    if let window = notification.object as? NSWindow {
        // Check if this window was closed by comparing `window.identifier`
    }
}
Carma answered 22/8 at 8:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.