Reputation: 188
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)
}
}
So, how should I stop the timer correctly after the window closed?
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. )
Upvotes: 3
Views: 1020
Reputation: 11
I have a similar problem and I've chosen the following approach.
window.isReleasedWhenClosed = false
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
}
}
Upvotes: -1
Reputation: 188
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
Upvotes: 1
Reputation: 258237
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)
}
}
Upvotes: 4