11
Timers
Written by Florent Pillet
Repeating and non-repeating timers are always useful when coding. Besides executing code asynchronously, you often need to control when and how often a task should repeat.
Before the Dispatch
framework was available, developers relied on RunLoop
to asynchronously perform tasks and implement concurrency. Timer
(NSTimer
in Objective-C) could be used to create repeating and non-repeating timers. Then Dispatch
arrived and with it, DispatchSourceTimer
.
Although all of the above are capable of creating timers, not all timers are equal in Combine. Read on!
Using RunLoop
The main thread and any thread you create, preferably using the Thread
class, can have its own RunLoop
. Just invoke RunLoop.current
from the current thread: Foundation would create one for you if needed. Beware, unless you understand how run loops operate — in particular, that you need a loop that runs the run loop — you’ll be better off simply using the main RunLoop
that runs the main thread of your application.
Note: One important note and a red light warning in Apple’s documentation is that the
RunLoop
class is not thread-safe. You should only callRunLoop
methods for the run loop of the current thread.
RunLoop
implements the Scheduler
protocol you’ll learn about in Chapter 17, “Schedulers.” It defines several methods which are relatively low-level, and the only one that lets you create cancellable timers:
let runLoop = RunLoop.main
let subscription = runLoop.schedule(
after: runLoop.now,
interval: .seconds(1),
tolerance: .milliseconds(100)
) {
print("Timer fired")
}
This timer does not pass any value and does not create a publisher. It starts at the date specified in the after:
parameter with the specified interval
and tolerance
, and that’s about it. Its only usefulness in relation to Combine is that the Cancellable
it returns lets you stop the timer after a while.
An example of this could be:
runLoop.schedule(after: .init(Date(timeIntervalSinceNow: 3.0))) {
cancellable.cancel()
}
But all things considered, RunLoop
is not the best way to create a timer. You’ll be better off using the Timer
class!
Using the Timer class
Timer
is the oldest timer that was available on the original Mac OS X, long before it was renamed “macOS.” It has always been tricky to use because of its delegation pattern and tight relationship with RunLoop
. Combine brings a modern variant you can directly use as a publisher without all the setup boilerplate.
let publisher = Timer.publish(every: 1.0, on: .main, in: .common)
let publisher = Timer.publish(every: 1.0, on: .current, in: .common)
let publisher = Timer
.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
let subscription = Timer
.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.scan(0) { counter, _ in counter + 1 }
.sink { counter in
print("Counter is \(counter)")
}
Using DispatchQueue
You can use a dispatch queue to generate timer events. While the Dispatch framework has a DispatchTimerSource
event source, Combine surprisingly doesn’t provide a timer interface to it. Instead, you’re going to use an alternative method to generate timer events in your queue. This can be a bit convoluted, though:
let queue = DispatchQueue.main
// 1
let source = PassthroughSubject<Int, Never>()
// 2
var counter = 0
// 3
let cancellable = queue.schedule(
after: queue.now,
interval: .seconds(1)
) {
source.send(counter)
counter += 1
}
// 4
let subscription = source.sink {
print("Timer emitted \($0)")
}
Key points
- Create timers using good old
RunLoop
class if you have Objective-C code nostalgia. - Use
Timer.publish
to obtain a publisher which generates values at given intervals on the specifiedRunLoop
. - Use
DispatchQueue.schedule
for modern timers emitting events on a dispatch queue.
Where to go from here?
In Chapter 18, “Custom Publishers & Handling Backpressure,” you’ll learn how to write your own publishers, and you’ll create an alternative timer publisher using DispatchSourceTimer
.