// // RunFlowView.swift // Workouts Watch App // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import SwiftUI /// Thin coordinator that lets a single pushed run walk across exercises in a flow-mode /// split (`autoAdvance`). It owns which log is on screen and swaps it when the running /// `ExerciseProgressView` finishes an exercise; `.id(currentLogID)` rebuilds the child /// fresh for each exercise, so the delicate per-exercise run/mirror machinery stays /// exactly as it is. For a non-flow split it's transparent — one exercise, unchanged. struct RunFlowView: View { @Environment(WatchConnectivityBridge.self) private var bridge @Binding var doc: WorkoutDocument let startLogID: String let onChange: () -> Void let onLive: (LiveProgress) -> Void /// Send the "stop mirroring this run" signal for a given log — parameterized because /// the run's current log advances; the leaving exercise's id is what ends. let sendLiveEnded: (String) -> Void @State private var currentLogID: String /// True once we've auto-advanced at least once — the current exercise then skips its /// Ready page and begins on appear. The first exercise (manual entry) keeps Ready. @State private var enteredViaFlow = false /// Set across an exercise hand-off so the leaving exercise's teardown doesn't tear the /// mirror down — the run is continuing, not ending. @State private var advancing = false init(doc: Binding, startLogID: String, onChange: @escaping () -> Void, onLive: @escaping (LiveProgress) -> Void, sendLiveEnded: @escaping (String) -> Void) { self._doc = doc self.startLogID = startLogID self.onChange = onChange self.onLive = onLive self.sendLiveEnded = sendLiveEnded _currentLogID = State(initialValue: startLogID) } var body: some View { // The ZStack is a stable identity (its lifecycle fires once for the whole run), // while the child re-identifies per exercise via `.id(currentLogID)`. ZStack { ExerciseProgressView( doc: $doc, logID: currentLogID, onChange: onChange, onLive: onLive, onLiveEnded: { if !advancing { sendLiveEnded(currentLogID) } }, incomingFrame: bridge.liveIncoming.flatMap { $0.logID == currentLogID ? $0 : nil }, enteredViaFlow: enteredViaFlow, onAdvance: advance(to:) ) .id(currentLogID) } .onAppear { bridge.navigatedRunID = currentLogID } .onDisappear { if bridge.navigatedRunID == currentLogID { bridge.navigatedRunID = nil } } } /// Hand off to the next exercise. Suppresses the leaving exercise's live-ended teardown /// so the mirror follows across the boundary, moves `navigatedRunID` forward so this /// device doesn't mirror its own next exercise, then swaps the presented log. private func advance(to next: String) { advancing = true bridge.navigatedRunID = next enteredViaFlow = true currentLogID = next // Release the guard after the swap settles (old view torn down, new one begun). Task { @MainActor in advancing = false } } }