How Musklr Keeps Your Apple Watch and iPhone in Sync During a Workout | Musklr Blog
Skip to content
How Musklr Keeps Your Apple Watch and iPhone in Sync During a Workout
By Musklr Team · Published 20 July 2026
This is what logging a set looks like when your phone stays in your pocket. Dial the weight in with the crown, tap once to log it, keep lifting. For that to feel instant, the watch and the phone have to agree on the truth the whole time. Here is how we made them agree, and the five sync bugs we had to kill first.
Logging a leg press from the wrist in Musklr, no phone in hand.
How the sync actually works
The design is deliberately one-sided. The iPhone owns every piece of workout state. It builds a WorkoutSnapshot, one struct that carries the complete display state of the workout, and that snapshot is the single source of truth. The Apple Watch is a read-only display. It renders the latest snapshot and nothing else. When you turn the crown or tap a button on your wrist, the Watch doesn't change any state of its own. It sends a small WatchCommand back to the phone and waits for the phone to send a new snapshot.
The wire between them is WCSession. The phone pushes each snapshot over updateApplicationContext, which the system holds and delivers even if the Watch is asleep or out of range, always keeping only the latest value. The Watch sends its commands back over sendMessage. Two types, one wire, one owner of the truth. Almost every bug below is a version of the same mistake: one side trusting a copy of the truth that was already out of date.
How Musklr keeps the iPhone and Apple Watch in sync<br>On the iPhone, WorkoutSessionManager builds a WorkoutSnapshot, the single source of truth for the workout. WCSession pushes it to the Watch over two transports at once: the guaranteed, coalesced updateApplicationContext, and a faster sendMessage path used only while the Watch is reachable. The Watch is a read-only display. It sends small WatchCommands back to the iPhone over sendMessage.
IPHONE
WorkoutSessionManager<br>builds
WorkoutSnapshot<br>single source of truth
WCSESSION
updateApplicationContext<br>guaranteed · OS-coalesced · latest value wins<br>sendMessage<br>fast path · Watch reachable only · near-immediate<br>WatchCommand<br>via sendMessage · queued if unreachable
APPLE WATCH
PhoneConnectivityManager<br>receives
WatchSessionViewModel<br>read-only display
The iPhone owns all state. WorkoutSessionManager builds the WorkoutSnapshot and pushes it to the Watch over two transports at once: the guaranteed, coalesced updateApplicationContext, and a faster sendMessage path used only while the Watch is reachable. The Watch never writes its own copy; it only sends WatchCommands back.
That single source of truth is what makes the wrist feel instant. Musklr is free on the App Store, two taps to log a set.
Get Musklr free<br>See everything Musklr does →
Five sync bugs we found and killed
Every one of these shipped to nobody. We caught them building the wrist experience, and each turned out to be the same shape once we looked: the Watch and the phone briefly disagreeing about what was true. Here they are, symptom, cause, and fix.
Bug 1: The weight that snapped back to zero
Symptom. You scroll the crown to 100 kg on your wrist. It reads 100. A second or two later, with your hand nowhere near the watch, it jumps back to zero.
Cause. Every 15 seconds the phone re-pushes its last snapshot as a heartbeat, so a Watch that missed an update still converges. The trouble was that the cached snapshot still held the value from before your edit. When the heartbeat landed, the Watch treated it as fresh truth and cleared the optimistic value you had just dialed in.
Fix. Two halves. On the phone, when a crown edit arrives, we patch the cached lastPushedSnapshot in place so the next heartbeat carries your current value instead of the stale one. On the Watch, an optimistic value is cleared only when the phone confirms it (a snapshot for the same set carrying that value), or when the edit stops being relevant (the set changed, or you left the active-set screen). A heartbeat can no longer wipe an edit the phone hasn't confirmed.
Here is the Watch-side reconcile, alongside the phone-side patch that keeps the heartbeat honest:
// Source:<br>// MusklrWatch Watch App/Services/WatchSessionViewModel.swift<br>// Musklr/Services/Workout/WorkoutSnapshotPublisher.swift<br>//<br>// Beat: the Watch clears an optimistic crown-scroll value ONLY when the<br>// phone CONFIRMS it (a snapshot for the same set carrying that value) — or<br>// the edit becomes irrelevant (the set changed, or we left active-set) —<br>// never on a stale 15s heartbeat re-push. The phone-side companion keeps its<br>// own cached `lastPushedSnapshot` patched with in-flight edits so that same<br>// heartbeat re-pushes the CURRENT value instead of the pre-edit one.<br>// (commits 5fad1d8b, dd3c1b57)
import Foundation
// stub: trimmed WorkoutPhase — the real enum (MusklrShared/Models/WorkoutPhase.swift)<br>// has...