// // LiveRunCoverView.swift // Workouts // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import SwiftUI import SwiftData /// Full-screen surface shown on a propped-up iPhone while the Apple Watch is running a live /// exercise (see `LiveProgress`). Rather than a read-only mirror, this presents the iPhone's /// real `ExerciseProgressView` driver, seeded to the watch's current page — so either device /// can drive the run and the other follows: /// /// • Incoming watch frames jump this driver's page (it never re-broadcasts them). /// • A human transition *here* broadcasts to the watch and persists durably via `SyncEngine`, /// exactly as the watch's own driver does. /// /// Auto-advances (rest / timed-work end) are never sent either way — both devices reach them /// independently off the shared wall-clock anchors carried in each frame. struct LiveRunCoverView: View { /// The frame that triggered presentation — fixes which workout/log this cover drives. let frame: LiveProgress /// Dismiss + suppress re-presentation of this run (the user tapped close). let onClose: () -> Void @Environment(SyncEngine.self) private var sync @Environment(AppServices.self) private var services @Environment(LiveRunState.self) private var liveRun @Environment(\.modelContext) private var modelContext /// The live workout entity, observed so durable updates the watch drove flow back into our /// working copy. Filtered to just this run's id. @Query private var workouts: [Workout] /// Working copy the driver mutates; resolved from the cache on appear. `nil` until then. @State private var doc: WorkoutDocument? init(frame: LiveProgress, onClose: @escaping () -> Void) { self.frame = frame self.onClose = onClose let workoutID = frame.workoutID _workouts = Query(filter: #Predicate { $0.id == workoutID }) } private var workout: Workout? { workouts.first } /// The latest frame to follow, scoped to this cover's run. private var incomingFrame: LiveProgress? { liveRun.current.flatMap { $0.logID == frame.logID ? $0 : nil } } var body: some View { NavigationStack { Group { if let binding = Binding($doc) { ExerciseProgressView( doc: binding, logID: frame.logID, onChange: { persist() }, onLive: { services.watchBridge.sendLiveProgress($0) }, onLiveEnded: { services.watchBridge.sendLiveEnded(workoutID: frame.workoutID, logID: frame.logID) }, incomingFrame: incomingFrame ) } else { // The workout hasn't resolved from the cache yet (or has vanished). ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) } } .toolbar { ToolbarItem(placement: .topBarLeading) { Button(action: onClose) { Image(systemName: "xmark") } .accessibilityLabel("Close") } } } .onAppear { if doc == nil, let workout { doc = WorkoutDocument(from: workout) } } .onChange(of: workout?.updatedAt) { _, _ in // Absorb the durable progress the watch drove (currentStateIndex / status). The // live *page* is driven by frames, not this — so re-seeding the doc is safe. if let workout { doc = WorkoutDocument(from: workout) } } } private func persist() { guard let doc else { return } let snapshot = doc Task { await sync.save(workout: snapshot) } } }