Sitemap
Parable Engineering

Building mobile computer vision + AI powered asynchronous telemedicine focused on wound care, post-acute care, & collaboration within decentralized teams. Augment + standardize care with powerful machine intelligence that seamlessly adapts to your existing workflows & practices!

Stop using Timer.publish in your SwiftUI views

4 min readMay 20, 2025

--

Usage example for the reusable `.onTimer` modifier

If you ever try to figure out how to use Foundation’s Timer in SwiftUI, the first thing you’ll see will be something like this:

struct CurrentDateView: View {
@State private var currentDate = Date.now

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

var body: some View {
Text(currentDate.formatted(date: .numeric, time: .standard))
.onReceive(timer) { input in
currentDate = input
}
}
}

You create a Combine publisher directly via Timer.publish function, and then “listen” to it with the onReceive modifier on your view.

And while there’s nothing particularly wrong with this (it works exactly as it should), I’d argue there’s a better way to achieve this.

Two things that are imperfect about this solution, in my opinion, are:

  1. Because of all the peculiarities of Timer publisher (remembering what run loop modes to use, and remembering the autoconnect() call), the API feels unintuitive and you’ll likely end up googling this snippet every time.
  2. Having a timer publisher as a free-standing property of your View adds unnecessary clutter and complexity for something that should be quite simple, and feels out of place in declarative SwiftUI code.

Solution: reusable TimerModifier

A much better solution is to encapsulate this logic in a ViewModifier that you can then easily reuse in any part of your app. The end result will look like this:

struct CurrentDateView: View {
@State private var currentDate = Date.now

var body: some View {
Text(currentDate.formatted(date: .numeric, time: .standard))
.onTimer(every: 1) { date in
currentDate = date
}
}
}

Doesn’t it look and feel much cleaner?

The implementation itself is also trivial, because we’re basically encapsulating our publisher logic within a separate ViewModifier

struct TimerModifier: ViewModifier {
@State private var timer: Publishers.Autoconnect<Timer.TimerPublisher>
private let perform: (Date) -> Void

init(
every interval: TimeInterval,
tolerance: TimeInterval?,
perform: @escaping (Date) -> Void
) {
self.timer = Timer.publish(every: interval, tolerance: tolerance, on: .main, in: .common)
.autoconnect()
self.perform = perform
}

func body(content: Content) -> some View {
content
.onReceive(timer) { date in
self.perform(date)
}
}
}
extension View {
func onTimer(
every interval: TimeInterval,
tolerance: TimeInterval? = nil,
perform: @escaping (Date) -> Void
) -> some View {
self.modifier(
TimerModifier(
every: interval,
tolerance: tolerance,
perform: perform
)
)
}
}
// usage:
Text(currentDate.formatted(date: .numeric, time: .standard))
.onTimer(every: 1, tolerance: 0.05) { date in
currentDate = date
}

As you see, we can also easily supply tolerance to our timer with this modifier.

In my opinion, this is one of the under-appreciated features of the ViewModifier — they can contain their own internal @State and all other SwiftUI observations, and can nicely encapsulate complicated logic.

Now you can use .onTimer API in any part of your app, without ever needing to look up how to use it, or caring about the intricacies of its Combine API.

Bonus 1: Cancellation

Cancelling a Timer publisher is even more of a hassle because you need to remember this exciting line:

timer.upstream.connect().cancel()

To incorporate cancellations into our onTimer modifier, we will use something called a “Trigger value pattern”. It’s used in a bunch of Apple’s own APIs and you can find a great overview of it here: Trigger value pattern in SwiftUI.

In short, we will add a cancelTrigger property to our TimerModifier, and monitor its change via the onChange modifier, so whenever the cancelTrigger value changes, we will use it to cancel the underlying Timer.

struct TimerModifier: ViewModifier {
@State private var timer: Publishers.Autoconnect<Timer.TimerPublisher>
let cancelTrigger: AnyHashable?
private let perform: (Date) -> Void

init(
every interval: TimeInterval,
tolerance: TimeInterval?,
cancelTrigger: AnyHashable?,
perform: @escaping (Date) -> Void
) {
self.timer = Timer.publish(every: interval, tolerance: tolerance, on: .main, in: .common)
.autoconnect()
self.cancelTrigger = cancelTrigger
self.perform = perform
}

func body(content: Content) -> some View {
content
.onReceive(timer) { date in
self.perform(date)
}
.onChange(of: cancelTrigger) {
self.timer.upstream.connect().cancel()
}
}
}
extension View {
func onTimer(
every interval: TimeInterval,
tolerance: TimeInterval? = nil,
cancelTrigger: AnyHashable? = nil, // providing a trigger is optional
perform: @escaping (Date) -> Void
) -> some View {
self.modifier(
TimerModifier(
every: interval,
tolerance: tolerance,
cancelTrigger: cancelTrigger,
perform: perform
)
)
}
}

And this is how we can cancel a Timer by pressing a button

struct CurrentDateView: View {
@State private var currentDate = Date.now
@State private var cancelTimer = false

var body: some View {
VStack {
Text(currentDate.formatted(date: .numeric, time: .standard))
Button("Stop Timer") {
cancelTimer = true
}
}
.onTimer(every: 1, cancelTrigger: cancelTimer) { date in
currentDate = date
}
}
}

Of course, the timer is also automatically cancelled when the View is removed from the render tree.

Conclusion

The main idea behind this article is not just to help you simplify the usage of the Timer in SwiftUI, but to generally suggest using view modifiers to encapsulate complicated logic. We all should strive to make our views as clean and readable as possible, and creating reusable view modifiers are a great way to achieve that.

A nicely packaged and extended version of this solution is available as a GitHub gist:

--

--

Parable Engineering
Parable Engineering

Published in Parable Engineering

Building mobile computer vision + AI powered asynchronous telemedicine focused on wound care, post-acute care, & collaboration within decentralized teams. Augment + standardize care with powerful machine intelligence that seamlessly adapts to your existing workflows & practices!

Oleg Dreyman
Oleg Dreyman

Written by Oleg Dreyman

iOS development know-it-all. Talk to me about Swift, coffee, photography & motorsports

Responses (3)