Search code examples
swifttimerbackground-threadrunloop

Swift scheduled timer method isn't called after time interval


I have such code to call Timer scheduled method. As far I've never had a problem with timer methods being called. I've even didn't used RunLoop.

private func expireToken(afterTimeInterval timeInterval: TimeInterval) {
        print("[DEBUG] Schedule expiration, time: \(timeInterval), main thread \(Thread.isMainThread)")
        let timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { timer in
            print("[DEBUG] Expiring token")
            self.tokenContainer = .expired
            timer.invalidate()
        }
        RunLoop.current.add(timer, forMode: .common)
    }

This method seems to be called from background thread as first print statement logs to console such information

[DEBUG] Schedule expiration, time: 10.0, main thread false

Solution

  • A Timer can only be scheduled on a RunLoop that is processed (regularly call one of the run... methods). Most threads do not process a RunLoop.

    Generally the right answer for something like what you've described is to run the timer on the main runloop. Timers themselves are extremely cheap.

    With Swift concurrency, you can also easily do this with a Task and Task.sleep. This will let you .cancel() the task exactly as you .invalidate() a Timer. I would recommend this approach in new code. Something like:

    private func expireToken(afterTimeInterval timeInterval: TimeInterval) {
        print("[DEBUG] Schedule expiration, time: \(timeInterval), main thread \(Thread.isMainThread)")
        // Be sure to cancel any previous expiration.
        self.expireTask?.cancel() 
        self.expireTask = Task {
            try Task.sleep(for: .seconds(timeInterval)
            // If the sleep is cancelled, the above line will throw
            // and these will not run.
            print("[DEBUG] Expiring token")
            self.tokenContainer = .expired
        }
    }
    

    You can also use DispatchQueue.asyncAfter to run something after a given time without a Timer.

    And finally, if you do need to process your own background RunLoop, see Run Loops in the Threading Programming Guide. This is rarely needed, however.

    As an unrelated note, there is no need to invalidate a timer that has already fired and does not repeat. It invalidates itself:

    A nonrepeating timer fires once and then invalidates itself automatically, thereby preventing the timer from firing again