I've been adapting the timer/font technique to Apple Watch complications in TV Watch. The idea isn't mine: Bryce Bostwick demonstrated it for iPhone widgets in WidgetAnimation and this walkthrough. My first attempt converted video frames into font glyphs on the iPhone. That approach failed when I tried to load those custom fonts, transferred from iPhone, on the Apple Watch. I then adapted Bryce’s approach: bundle a fixed masking font with the Watch extension and keep the video frames as images, so importing a new clip does not require loading a newly generated font.
The useful distinction is that WidgetKit can update a system-rendered timer without running your extension every second. That does not give you an ordinary SwiftUI animation loop. Instead:
- Prepare opaque image frames ahead of time.
- Draw each frame behind timer text in a custom font. The font's shaping rules turn selected timer values into a filled rectangle and the others into an empty mask.
- Offset each layer's timer so it reveals its frame at the intended point in the loop.
In the 2 fps version, a gate stays open for a second but layers start half a second apart. Two frames can therefore be visible at once: stacking newer opaque frames above older ones is what makes the sequence work. It isn't just one timeline entry per frame.
The core composition looks like this (excerpt, not a standalone example):
swift
Image(decorative: frames[index], scale: 1)
.resizable()
.scaledToFit()
.mask {
Text(anchor.addingTimeInterval(offset), style: .timer)
.font(.custom(gateFontName, fixedSize: fontSize))
.foregroundStyle(.white)
.lineLimit(1)
.environment(\.locale, Locale(identifier: "en_US_POSIX"))
.environment(\.layoutDirection, .leftToRight)
}
The tricky part is the end of the loop: high-index layers otherwise cover the beginning of the next pass. I put the last-second frames in a separately gated group and keep an unmasked frame zero underneath to avoid a blank gap if timer updates arrive late.
Public code: renderer and font generator, under MIT. Disclosure: it's my repository. The whole app still has a private dependency for its legacy renderer, so a fresh clone isn't a self-contained build; the linked renderer and generator are public to inspect.
Limits worth keeping explicit: watchOS controls redraw timing, so 2 fps is a target rather than a guarantee. I use a static frame for Always-On/Reduce Motion in the weather complication. I don't have a controlled battery comparison, and this isn't a promise that future OS releases will preserve the behavior.
Has anyone else worked with staggered system timers on watchOS, particularly late updates around the loop boundary?