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
@@ -86,7 +86,13 @@ struct WorkoutLogListView: View {
}
.navigationTitle(doc.splitName ?? Split.unnamed)
.navigationDestination(item: $selectedLogID) { logID in
ExerciseProgressView(doc: $doc, logID: logID, onChange: { bridge.update(workout: doc) })
ExerciseProgressView(
doc: $doc,
logID: logID,
onChange: { bridge.update(workout: doc) },
onLive: { bridge.sendLiveProgress($0) },
onLiveEnded: { bridge.sendLiveEnded(workoutID: doc.id, logID: logID) }
)
}
.sheet(isPresented: $showingExercisePicker) {
ExercisePickerView(exercises: availableExercises) { exercise in