What is the best way to trigger a function/block at a precise (within 1 second) date/time in Swift on iOS?
I’ve tried using Timer
’s init
with fireAt
, but I’m finding that the precision is often off by a few seconds. Setting Timer precision to a value like 0.5 has not yielded better results.
I’ve also tried using GCD with DispatchQueue.main.asyncAfter(.now() someDelay)
and that is also often off by a few seconds in my testing.
What I’ve settled on is creating a Timer that fires once per second and then I’m checking if the current date/time has elapsed my desired value and then I manually trigger my code. It seems like there might be a better way to go about this, but I’m out of ideas. Thanks!
CodePudding user response:
If you make a GCD timer with makeTimerSource
with a flag of .strict
, it will opt out of timer coalescing:
The system makes its best effort to observe the timer’s specified
leeway
value, even if the value is smaller than the default leeway.
For example:
var timer: DispatchSourceTimer?
func startTimer(in interval: DispatchTimeInterval, block: @escaping () -> Void) {
timer = DispatchSource.makeTimerSource(flags: .strict, queue: .main)
timer?.setEventHandler(handler: block)
timer?.schedule(deadline: .now() interval, leeway: .milliseconds(500))
timer?.activate()
}
And then:
startTimer(in: .seconds(240)) { [weak self] in
self?.foo()
}
Note, unlike a Timer
, which is retained by the RunLoop
upon which you schedule it, you have to keep your own strong reference to the GCD timer, as shown above. You might make Timer
properties weak
(so that they are deallocated when the RunLoop
releases them), but you want strong references to GCD timers.
A few caveats:
Note the use of
[weak self]
capture list. You generally want to avoid strong reference cycle (especially if the timer was a repeating timer).This pattern should be employed only when absolutely needed, as you are opting out of the power saving features offered by timer coalescing. I would expect that this is more power efficient than the repeating timer approach, but less power efficient than letting the OS use its discretion to coalesce its timers.
This pattern assumes that the queue on which it is scheduled is not blocked. Obviously, we never block the main thread, but you can always consider using your own target queue.
For the sake of future readers, this pattern obviously applies when the app is actually running. If the app is suspended, timers cannot fire. If you want the user to be notified at the appointed time even if the app is not currently running, you would use a locally scheduled user notification.