Sequential Timer
A ritual timekeeper for the iPhone and Watch.
- Swift
- SwiftUI
- watchOS
I don't remember my own workouts. Halfway through a session I've lost track of what's next, how many sets are left, how long the rest should be. So I check my phone, and two minutes later I'm still holding it. A short session has sprawled.
Another interval timer doesn't fix that. It still expects me to remember. I wanted something that holds the plan and paces it from the wrist.
You build an Hour on the iPhone. An Hour is a run of beats grouped into sections, and a section repeats, which is how eight rounds of Tabata is two rows in the editor instead of sixteen. A beat has a length, or it's an untimed task you clear by tapping Done. It carries a weight from Calm to Grueling, which colours the run screen so you brace early. It can ping at halfway and speak its final seconds aloud. Hours group into an Office, an ordered set with a finish line. Fifteen patterns ship with it, Tabata and Norwegian 4x4 among them.
Then you run it from the Watch.
The bells are generated, not sampled
Ten bell voices, none of them a recording. Each is a spec: a fundamental, a list of partials, a waveform and gain per partial, an attack, a decay, then optionally a vibrato, a pitch chirp or a wooden knock. The app adds those oscillators into a WAV in memory and plays that. On the phone a live ambient bed runs under the session, collapsing its low-pass filter when a heavy beat drops into a long rest.
The spoken cues are my own voice. Seventy-two clips from a local Fish-Speech server, each filed under the first twenty hex characters of the sha256 of the phrase it says. It hashes what it's about to speak and plays that file if it exists. A label I typed myself has no clip, so it falls through to the system synthesiser.
The wrist runs it, the phone builds it
The Watch app runs Hours and can't build them. Its library says "Inscribe on your iPhone." That's the design, not a shortcut. The phone owns the library, the wrist owns its runs, and sync merges by union so a run sealed on the watch survives the next push. A watch killed on the seal screen used to lose that run, so the outbox now writes to disk and drains on reconnect.
What the simulator was hiding
The Watch declared WKBackgroundModes as extended-runtime. That isn't one of Apple's documented values, so the session invalidates the instant it starts. The code only logged it. Every run would have suspended the moment my wrist dropped. The simulator doesn't enforce the key, so nothing looked wrong. It's mindfulness now, a real session type.
A suspended app also stops ticking, so the runner watches for a big overshoot and jumps silently to the beat the wall clock is in.
Both came out of an audit that found three critical and fourteen major defects. They're fixed and both schemes build. But the checking was builds and the simulator, and the point of the first fix is that the simulator lies. Neither is confirmed on hardware.
What it doesn't do
No reps, no weights, no exercise log. It records which Hours were kept on which day. It paces a sequence I already decided on, not a training database. iPhone only, because iPad is untested and a device family can't be removed once shipped. Those are decisions.
Cloud sync is a gap. The backend is twelve Postgres migrations verified against a real Postgres 16. It isn't deployed, the client config is empty, and the networking layer no-ops.
Pre-release. The flag that lifts every limit for testing is still on, with a compiler warning on every build so I can't archive it by accident. A full gym session paced from the wrist is still the target, not a result. Nothing to download.
Stack: Swift and SwiftUI across four targets sharing one core, generated by XcodeGen from one project.yml, the only way I've kept four targets honest. SwiftData for the model, WatchConnectivity for sync, AVAudioPlayer for the rendered bells and AVAudioEngine for the ambient bed.