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:
@@ -16,6 +16,8 @@ enum WCPayload {
|
||||
|
||||
static let workoutUpdateType = "workoutUpdate" // watch → phone (one workout)
|
||||
static let requestSyncType = "requestSync" // watch → phone (please push state)
|
||||
static let liveProgressType = "liveProgress" // watch → phone (ephemeral mirror frame)
|
||||
static let liveEndedType = "liveEnded" // watch → phone (stop mirroring a run)
|
||||
|
||||
// MARK: - Phone → Watch (application context: latest-state-wins)
|
||||
|
||||
@@ -66,4 +68,62 @@ enum WCPayload {
|
||||
}
|
||||
|
||||
static func requestSyncMessage() -> [String: Any] { [typeKey: requestSyncType] }
|
||||
|
||||
// MARK: - Watch → Phone (ephemeral live-run mirror)
|
||||
|
||||
/// Anchor `Date`s ride as native plist values (not JSON), so they keep sub-second
|
||||
/// precision — `DocumentCoder` is `.iso8601`, which would round the timer off.
|
||||
static let lpWorkoutIDKey = "lpWorkoutID"
|
||||
static let lpLogIDKey = "lpLogID"
|
||||
static let lpNameKey = "lpName"
|
||||
static let lpPhaseKey = "lpPhase"
|
||||
static let lpSetIndexKey = "lpSetIndex"
|
||||
static let lpSetCountKey = "lpSetCount"
|
||||
static let lpDetailKey = "lpDetail"
|
||||
static let lpStartKey = "lpStart"
|
||||
static let lpEndKey = "lpEnd"
|
||||
static let lpVersionKey = "lpVersion"
|
||||
|
||||
static func encodeLiveProgress(_ p: LiveProgress) -> [String: Any] {
|
||||
var dict: [String: Any] = [typeKey: liveProgressType]
|
||||
dict[lpWorkoutIDKey] = p.workoutID
|
||||
dict[lpLogIDKey] = p.logID
|
||||
dict[lpNameKey] = p.exerciseName
|
||||
dict[lpPhaseKey] = p.phase.rawValue
|
||||
dict[lpSetIndexKey] = p.setIndex
|
||||
dict[lpSetCountKey] = p.setCount
|
||||
dict[lpDetailKey] = p.detail
|
||||
dict[lpStartKey] = p.phaseStart
|
||||
if let end = p.phaseEnd { dict[lpEndKey] = end }
|
||||
dict[lpVersionKey] = p.version
|
||||
return dict
|
||||
}
|
||||
|
||||
static func decodeLiveProgress(_ dict: [String: Any]) -> LiveProgress? {
|
||||
guard let workoutID = dict[lpWorkoutIDKey] as? String,
|
||||
let logID = dict[lpLogIDKey] as? String,
|
||||
let phaseRaw = dict[lpPhaseKey] as? String,
|
||||
let phase = LiveRunPhase(rawValue: phaseRaw),
|
||||
let setIndex = dict[lpSetIndexKey] as? Int,
|
||||
let setCount = dict[lpSetCountKey] as? Int,
|
||||
let start = dict[lpStartKey] as? Date,
|
||||
let version = dict[lpVersionKey] as? Int
|
||||
else { return nil }
|
||||
return LiveProgress(
|
||||
workoutID: workoutID,
|
||||
logID: logID,
|
||||
exerciseName: dict[lpNameKey] as? String ?? "",
|
||||
phase: phase,
|
||||
setIndex: setIndex,
|
||||
setCount: setCount,
|
||||
detail: dict[lpDetailKey] as? String ?? "",
|
||||
phaseStart: start,
|
||||
phaseEnd: dict[lpEndKey] as? Date,
|
||||
version: version
|
||||
)
|
||||
}
|
||||
|
||||
static func encodeLiveEnded(workoutID: String, logID: String) -> [String: Any] {
|
||||
[typeKey: liveEndedType, lpWorkoutIDKey: workoutID, lpLogIDKey: logID]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user