Mirror a live Apple Watch run on a propped-up iPhone

Add an ephemeral live-run presence channel (separate from the durable
iCloud progress sync) so a propped-up iPhone can mirror the Watch's
Ready → work/rest → Finish flow in real time as the user swipes.

Watch drives, phone mirrors (read-only), so there's no echo loop:
- Watch's ExerciseProgressView broadcasts a LiveProgress frame on every
  phase transition (and an ended signal on leave) via sendMessage,
  reachable-only — throwaway presence, never written to iCloud.
- Timers ride as wall-clock anchors (Date kept native in the WC dict to
  preserve sub-second precision), so both devices count independently
  off shared start times and stay in lockstep without streaming ticks.
- Phone holds a transient LiveRunState; ContentView auto-presents a
  read-only LiveProgressMirrorView full-screen cover while a run is live.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
This commit is contained in:
2026-06-20 21:08:32 -04:00
parent 8ef0e96b31
commit a16e8ec270
13 changed files with 531 additions and 4 deletions
+6 -1
View File
@@ -13,13 +13,18 @@ final class AppServices {
let watchBridge: PhoneConnectivityBridge
let workoutLauncher = WorkoutLauncher()
/// Ephemeral live-run state fed by the watch, observed by the mirror UI. Not persisted.
let liveRunState: LiveRunState
private var bootstrapTask: Task<Void, Never>?
init() {
let container = WorkoutsModelContainer.make()
self.container = container
self.syncEngine = SyncEngine(container: container)
self.watchBridge = PhoneConnectivityBridge(container: container, syncEngine: syncEngine)
let liveRunState = LiveRunState()
self.liveRunState = liveRunState
self.watchBridge = PhoneConnectivityBridge(container: container, syncEngine: syncEngine, liveRunState: liveRunState)
#if DEBUG
if ScreenshotSeed.isActive { ScreenshotSeed.populate(container.mainContext) }
#endif