// // LiveRunCoverView.swift // Workouts Watch App // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import SwiftUI import SwiftData /// Follower surface shown on the watch when the *iPhone* starts driving a live exercise (see /// `LiveProgress`) and the watch isn't already in that run. It presents the watch's real /// `ExerciseProgressView`, seeded to the phone's current page — so the watch follows along and /// the user can take over and drive from the wrist, mirroring the iPhone's `LiveRunCoverView`. /// /// • Incoming phone frames jump this driver's page (it never re-broadcasts them). /// • A human transition *here* broadcasts back to the phone and persists through the bridge. struct LiveRunCoverView: View { /// The frame that triggered presentation — fixes which workout/log this cover follows. let frame: LiveProgress /// Dismiss + suppress re-presentation of this run (the user closed it). let onClose: () -> Void @Environment(WatchConnectivityBridge.self) private var bridge @Environment(\.modelContext) private var modelContext /// The live workout entity, filtered to this run's id and observed so durable updates the /// phone drove flow back into our working copy. @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? { bridge.liveIncoming.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: { bridge.sendLiveProgress($0) }, onLiveEnded: { bridge.sendLiveEnded(workoutID: frame.workoutID, logID: frame.logID) }, incomingFrame: incomingFrame ) } else { // The workout hasn't resolved from the cache yet (or has vanished). ProgressView() } } } .onAppear { if doc == nil, let workout { doc = WorkoutDocument(from: workout) } } .onChange(of: workout?.updatedAt) { _, _ in // Absorb the durable progress the phone drove. 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 } bridge.update(workout: doc) } }