Search code examples
macostimerswiftuinswindow

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


I use a Timer in View to show time. In the View's onAppear() and onDisappear() method, the Timer works well.

But when I close the window, it seems that the onDisappear() method not be called, and the Timer never stops.

There is my test code:

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. So, how should I stop the timer correctly after the window closed?

  2. And how could the View been notified when the window will be closed, aim to release some resources in the View instance.

( I have figured out a trick method using TimerPublisher replace Timer which would auto-stop after the window closed. But it doesn't resolve my confusion. )


Solution

  • Wow, I found out a more simple and clear solution.

    In the View struct, We could assign an NSWindowDelegate property which listening to the event of the hosting window and managing the resource objects that should be manually controlled.

    Example:

    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
            }
        }
    }
    

    And then in AppDelegate.swift, assign the View.windowDelegate property to NSWindow.delegate:

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