I'm building Publit, an open-source public transport app for iPhone and Apple Watch, written in SwiftUI. Almost every row shows a countdown like "in 4 min", so most of the UI depends on the current time.
The first version used `TimelineView(.periodic(from:by:))` on the departure and trip screens. Freeze logs from an early build showed the main thread stuck inside SwiftUI's update loop, in `requestUpdate(after:)`, while it drew the trip screen.
I replaced the timelines with a plain observable clock that ticks on whole-minute boundaries. One timer serves every row in a list, and thanks to Observation only the views that read `now` redraw on a tick:
```swift
u/MainActor u/Observable
final class Ticker {
static let minute = Ticker(every: 60)
private(set) var now = Date()
u/ObservationIgnored let interval: TimeInterval
u/ObservationIgnored private var loop: Task<Void, Never>?
init(every interval: TimeInterval) {
self.interval = interval
loop = Task { [weak self] in
while !Task.isCancelled {
guard let delay = self?.delayToNextTick() else { return }
try? await Task.sleep(for: .seconds(delay))
guard let self, !Task.isCancelled else { return }
self.now = Date()
}
}
}
private func delayToNextTick() -> TimeInterval {
Ticker.delay(toNextMultipleOf: interval, from: Date())
}
nonisolated static func delay(toNextMultipleOf interval: TimeInterval, from date: Date) -> TimeInterval {
let t = date.timeIntervalSinceReferenceDate
let next = (floor(t / interval) + 1) * interval
return max(0.05, next - t + 0.05)
}
}
```
For a small part of a screen that needs its own pace, a wrapper runs the same loop in `.task`, so it stops when the view leaves the screen:
```swift
Clocked(every: 30) { now in
LeaveHero(itinerary: itinerary, now: now, framed: false)
}
```
Two details mattered. The delay goes to the next whole multiple of the interval plus 50 ms, so the minute has really changed when views redraw. And the loop only holds `self` weakly, so a clock that nobody uses can go away.
Full file: https://github.com/elija-7580/Publit/blob/main/Sources/Views/Components/Ticker.swift
Repo (AGPL-3.0): https://github.com/elija-7580/Publit
Has anyone else hit this with TimelineView? I'd like to know whether I was using it wrong or whether a shared clock is the usual fix for long lists of countdowns.
Full disclosure: I build Publit with help from AI coding tools. I review, test and maintain it myself.Replacing TimelineView with one shared u/Observable clock in my transit app (open source)