Reputation: 952
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.
Upvotes: 1
Views: 909
Reputation: 26344
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.
Upvotes: 0
Reputation: 4301
Since keyToTimerInfoMap
is a private
property, which means, when deinit
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:
nonisolated(unsafe)
private var keyToTimerInfoMap = [String: TimerInfo]()
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 😉
Upvotes: 1