skip to Main Content

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


  1. 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 😉

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search