a16e8ec270
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
46 lines
1.6 KiB
Swift
46 lines
1.6 KiB
Swift
import Foundation
|
|
import Observation
|
|
import SwiftData
|
|
|
|
/// Composition root for the iOS app. Owns the SwiftData cache container and the
|
|
/// iCloud sync engine, and drives the one-shot launch sequence. Injected into the
|
|
/// view tree via `.environment(...)`.
|
|
@Observable
|
|
@MainActor
|
|
final class AppServices {
|
|
let container: ModelContainer
|
|
let syncEngine: SyncEngine
|
|
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)
|
|
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
|
|
}
|
|
|
|
/// Launch step: resolve iCloud and reconcile the cache. Idempotent — repeated
|
|
/// callers await the same one-shot task.
|
|
func bootstrap() async {
|
|
if let bootstrapTask { await bootstrapTask.value; return }
|
|
let task = Task { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
await self.syncEngine.connect()
|
|
self.watchBridge.activate()
|
|
}
|
|
bootstrapTask = task
|
|
await task.value
|
|
}
|
|
}
|