我在 SwiftUI 中的 macOS 应用程序,定时器在 window (NSWindow) 关闭后永不停止

My macOS app in SwiftUI, the Timer never stop after window (NSWindow) closed

我在视图中使用计时器来显示时间。在View的onAppear()和onDisappear()方法中,Timer效果很好

但是当我关闭 window 时,似乎没有调用 onDisappear() 方法,并且 Timer 永远不会停止。

有我的测试代码:

import SwiftUI
    
struct TimerTest: View {
    @State var date = Date()
    @State var showSubView = false
    @State var timer: Timer?
    
    var body: some View {
        ZStack{
            if showSubView {
                VStack {
                    Text(" Timer Stoped?")
                    Button("Back") {
                        self.showSubView = false
                    }
                }
            }
            else {
                VStack {
                    Button("Switch to subview"){
                        self.showSubView = true
                    }

                    Text("date: \(date)")
                        .onAppear(perform: {
                            self.timer = Timer.scheduledTimer(withTimeInterval: 1,
                                            repeats: true,
                                            block: {_ in
                                              self.date = Date()
                                              NSLog("onAppear timer triggered")
                                             })
                        })
                        .onDisappear(perform: {
                            self.timer?.invalidate()
                            self.timer = nil
                            NSLog(" onDisappear stop timer")
                            // But if I close window, this method never be called
                        })
                }
            }
        }
        .frame(width: 500, height: 300)
    }
}
  1. 那么,window关闭后应该如何正确停止定时器呢?

  2. 以及window关闭时如何通知View,旨在释放View实例中的一些资源。

(我想出了一个技巧,使用 TimerPublisher 替换 Timer,它会在 window 关闭后自动停止。但这并不能解决我的困惑。)

使用 .hostingWindow 环境(来自 ),可以使用以下方法。

测试 Xcode 11.4 / iOS 13.4

struct TimerTest: View {
    @Environment(\.hostingWindow) var myWindow
    @State var date = Date()
    @State var showSubView = false
    @State var timer: Timer?

    var body: some View {
        ZStack{
            if showSubView {
                VStack {
                    Text(" Timer Stoped?")
                    Button("Back") {
                        self.showSubView = false
                    }
                }
            }
            else {
                VStack {
                    Button("Switch to subview"){
                        self.showSubView = true
                    }

                    Text("date: \(date)")
                        .onAppear(perform: {
                            self.timer = Timer.scheduledTimer(withTimeInterval: 1,
                                            repeats: true,
                                            block: {_ in
                                              self.date = Date()
                                              NSLog("onAppear timer triggered")
                                             })
                        })
                        .onDisappear(perform: {
                            self.timer?.invalidate()
                            self.timer = nil
                            NSLog(" onDisappear stop timer")
                            // But if I close window, this method never be called
                        })
                }
                .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: myWindow())) { _ in
                    self.timer?.invalidate()
                    self.timer = nil
                }
            }
        }
        .frame(width: 500, height: 300)
    }
}

哇,我找到了一个更简单明了的解决方案。

在视图结构中,我们可以分配一个 NSWindowDelegate 属性 来监听托管事件 window 并管理应该手动控制的资源对象。

示例:

import SwiftUI
    
struct TimerTest: View {
    @State var date = Date()
    @State var showSubView = false

    // This windowDelegate listens to the window events 
    // and manages resource objects like a Timer.
    var windowDelegate: MyWindowDelegate = MyWindowDelegate()
    
    var body: some View {
        ZStack{
            if showSubView {
                VStack {
                    Text(" Timer Stoped?")
                    Button("Back") {
                        self.showSubView = false
                    }
                }
            }
            else {
                VStack {
                    Button("Switch to subview"){
                        self.showSubView = true
                    }

                    Text("date: \(date)")
                        .onAppear(perform: {
                            self.windowDelegate.timer = Timer.scheduledTimer(withTimeInterval: 1,
                                            repeats: true,
                                            block: {_ in
                                              self.date = Date()
                                              NSLog(" onAppear timer triggered")
                                             })
                        })
                        .onDisappear(perform: {
                            self.windowDelegate.timer?.invalidate()
                            self.windowDelegate.timer = nil
                            NSLog(" onDisappear stop timer")
                        })
                }
            }
        }
        .frame(width: 500, height: 300)
    }
    
    class MyWindowDelegate: NSObject, NSWindowDelegate {
        var timer: Timer?
        
        func windowWillClose(_ notification: Notification) {
            NSLog(" window will close. Stop timer")
            self.timer?.invalidate()
            self.timer = nil
        }
    }
}

然后在AppDelegate.swift中,将View.windowDelegate属性分配给NSWindow.delegate:

window.contentView = NSHostingView(rootView: contentView)
window.delegate = contentView.windowDelegate

我遇到了类似的问题,我选择了以下方法。

  1. window.isReleasedWhenClosed = false

  2. window.contentView = 无

    .onAppear() {
        print("onAppear...")
    }
    .onDisappear() {
        print("onDisappear...")
    }
    .doDisappearFromWillCloseNSWindow(window: window)



extension View {
@ViewBuilder
func doDisappearFromWillCloseNSWindow(window: Any?) -> some View { 
    #if os(macOS)
    let win = window as? NSWindow
    let noti = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: win)
    self.onReceive(noti) { _ in
        print("onReceive... willCloseNotification")
        win?.contentView = nil // <--- call onDisappear
    }
    #else
    self
    #endif
}

}