22
Jan
2021
Timers and countdowns in SwiftUI
Reading time: 1 min
This recipe shows how to implement a timer in SwiftUI in order to update the UI state at a specific interval.
There are two ways of going about it:
- The simple one, with
onReceive
. - A bit more complex, but also more powerful one, using
SimpleTimer
wrapper.
Direct updates with onReceive
Use onReceive to trigger an event whenever the timer Publisher
emits:
import Combine
...
@State private var elapsedTime = 0
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text("Elapsed time: \(elapsedTime) s")
}.onReceive(timer) { _ in
self.elapsedTime += 1
}
}
The SimpleTimer wrapper
Start off with this simple wrapper that wraps a TimerPublisher:
import Combine
class SimpleTimer {
// the interval at which the timer ticks
let interval: TimeInterval
// the action to take when the timer ticks
let onTick: () -> Void
private var timer: Publishers.Autoconnect<Timer.TimerPublisher>? = nil
private var subscription: AnyCancellable? = nil
init(interval: TimeInterval, onTick: @escaping () -> Void) {
self.interval = interval
self.onTick = onTick
}
var isRunning: Bool {
timer != nil
}
// start the timer and begin ticking
func start() {
timer = Timer.publish(every: interval, on: .main, in: .common).autoconnect()
subscription = timer?.sink(receiveValue: { _ in
self.onTick()
})
}
// cancel the timer and clean up its resources
func cancel() {
timer?.upstream.connect().cancel()
timer = nil
subscription = nil
}
}
Here's an example where the timer ticks every second:
@State private var remainingTime = 0
timer = SimpleTimer(interval: 1) {
self.remainingTime -= 1
}
Then, simply start it when needed:
timer.start()
And after you don't need it anymore, call cancel
:
timer.cancel()
Tip: TimerPublisher
emits the current date with every tick, and you can optionally consume that value as the parameter in onReceive
and sink(receiveValue:
blocks.