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:
@@ -23,6 +23,10 @@ final class WatchConnectivityBridge: NSObject {
|
||||
private(set) var editingWorkoutID: String?
|
||||
private(set) var editingSplitID: String?
|
||||
|
||||
/// Monotonic sequence stamped on each live-run frame, so the phone mirror can drop a
|
||||
/// stale / out-of-order delivery.
|
||||
private var liveVersion = 0
|
||||
|
||||
private var context: ModelContext { container.mainContext }
|
||||
|
||||
init(container: ModelContainer) {
|
||||
@@ -58,6 +62,26 @@ final class WatchConnectivityBridge: NSObject {
|
||||
sendToPhone(doc)
|
||||
}
|
||||
|
||||
// MARK: - Live run mirror (ephemeral; reachable-only)
|
||||
|
||||
/// Broadcast where the run flow currently is, so a propped-up iPhone can mirror it. Sent
|
||||
/// over `sendMessage` only when the phone is reachable — this is throwaway presence, so
|
||||
/// there's no guaranteed-delivery fallback (a queued frame would be stale on arrival).
|
||||
func sendLiveProgress(_ frame: LiveProgress) {
|
||||
guard let session, session.activationState == .activated, session.isReachable else { return }
|
||||
liveVersion += 1
|
||||
var stamped = frame
|
||||
stamped.version = liveVersion
|
||||
session.sendMessage(WCPayload.encodeLiveProgress(stamped), replyHandler: nil, errorHandler: { _ in })
|
||||
}
|
||||
|
||||
/// Tell the phone to stop mirroring this run (the user left the progress flow).
|
||||
func sendLiveEnded(workoutID: String, logID: String) {
|
||||
guard let session, session.activationState == .activated, session.isReachable else { return }
|
||||
session.sendMessage(WCPayload.encodeLiveEnded(workoutID: workoutID, logID: logID),
|
||||
replyHandler: nil, errorHandler: { _ in })
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
private func sendToPhone(_ doc: WorkoutDocument) {
|
||||
|
||||
Reference in New Issue
Block a user