I have a main actor isolated class like this one:
@MainActor
open class TimerScheduler {
private enum TimerState {
case normal
case paused(remaining: TimeInterval)
}
private struct TimerInfo {
let timer: Timer
let state: TimerState
}
private var keyToTimerInfoMap = [String:TimerInfo]()
private let lifecycleChecker: () -> Bool
public init(lifecycleChecker: @escaping @MainActor () -> Bool = { true }) {
self.lifecycleChecker = lifecycleChecker
// Note: When app is in background, NSTimer will only get a few minutes of execution.
// Then the timer will be paused
// These notifications are there to ensure NSTimer is paused immediately when app goes background.
NotificationCenter.default.addObserver(self, selector: #selector(pause), name:UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(resume), name:UIApplication.didBecomeActiveNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
for (_, info) in keyToTimerInfoMap {
info.timer.invalidate()
}
}
private func unschedule(key: String) {
guard let info = keyToTimerInfoMap[key] else { return }
info.timer.invalidate()
keyToTimerInfoMap.removeValue(forKey: key)
}
// other functions
}
I have a warning:
Cannot access property ‘keyToTimerInfoMap’ with a non-sendable type ‘[String : TimerScheduler.TimerInfo]’ from non-isolated deinit; this is an error in the Swift 6 language mode
This is reasonable because deinit
can happen in any thread.
Xcode also suggests to convert TimerInfo
Sendable, which I can’t do because Timer
is not Sendable.
I could wrap the deinit
in a main actor Task like this:
deinit {
NotificationCenter.default.removeObserver(self)
Task { @MainActor in
for (_, info) in keyToTimerInfoMap {
info.timer.invalidate()
}
}
}
However, if the object is deinit
‘ed in the background thread, I would be using the object (accessing its keyToTimerInfoMap
field) after it’s been deallocated, which seems to be a dangerous thing to do.
I am using Xcode 16 beta 3, Swift 5 with "complete" concurrency checking.
2
Answers
Since
keyToTimerInfoMap
is aprivate
property, which means, whendeinit
is executing it can’t be mutated from other place (thread/actor). We should be able to safely make the cleanup.And to disable static checking of data isolation we can use
nonisolated(unsafe)
property attribute introduced in Swift 5.10:P.S. I’m not super expert in this topic. So, if I’m wrong – please don’t hesitate to comment about what’s wrong 😉
Long story short is that you can’t. If you need something to be deallocated on the Main Actor you need to introduce a explicit
@MainActor func stop()
and call that before your object is deallocated by its owner (this can be necessary if you’re wrapping things like the AVPlayer).A better solution is to avoid having Main Actor isolated properties/objects that needs to be explicitly deallocated, like Silmaril suggested.
Consider not marking your entire class
@MainActor
as well, and only use it for the relevant (public) properties. There’s really nothing in the code you shown us that requires use of MainActor.