3

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. )

funway
  • 113
  • 1
  • 7
  • How does `TimerPublisher` solve the problem? You still need to `cancel` it. Perhaps you can post answer, below, showing how the publisher gets around the issue (your question about detecting the closing of the window notwithstanding). When I tested `TimerPublisher`, I saw the same behavior as above. – Rob Aug 03 '20 at 16:54
  • FYI, [this is my publisher test](https://gist.github.com/robertmryan/213c47fc379b6790615fc16390700736), suffering from the same problem as above. Clearly, [Asperi's solution below](https://stackoverflow.com/a/63232013/1271826) is the consensus approach to solving the broader question of detecting when a window is closed, but I'm just wondering what your "trick" was. – Rob Aug 03 '20 at 17:25
  • I use the TimerPublisher in onReceive() method like this: https://gist.github.com/funway/f88e80b93674bbb0fa83d080cf685476 – funway Aug 04 '20 at 00:49

3 Answers3

4

With usage of .hostingWindow environment (from How to access own window within SwiftUI view?) it is possible to use the following approach.

Tested with 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)
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thanks, Asperi. Your solution works. It's cool and a little complicated for me. I try a more simple way based on your solution, giving a `weak var myWindow: NSWindow?` as a stored property in the View, replace that Environment variable. The `onReceive` method will be called when the myWindow will close, It works like your solution. But if I use it with a TimerPublisher's receiver, the TimerPublisher can not auto stop. I have no idea why. In your solution, the TimerPublisher works well with NSWindow.willCloseNotification. Could you give me some tips about the difference? – funway Aug 04 '20 at 02:34
  • There is my simple solution: https://gist.github.com/funway/400a7b760348e7cf2978ddda1963868b – funway Aug 04 '20 at 02:37
  • @funway, at that time I started with `weak var` as well, but it introduced cycle reference with side effects like you see, so I finished with approach as shown and it works. – Asperi Aug 04 '20 at 03:56
1

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
funway
  • 113
  • 1
  • 7
-1

I have a similar problem and I've chosen the following approach.

  1. window.isReleasedWhenClosed = false

  2. window.contentView = nil

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

}

전상현
  • 1
  • 1
  • 3