diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f770ff..f809dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ All notable changes to this project are documented here. on the final set. - Added a configurable rest-between-sets duration (iPhone Settings, default 45s), synced to the watch over WatchConnectivity. +- Watch progress is now monotonic and reliably synced: completing a work phase + advances the set count on the iPhone and a finished set is never un-counted, and + reopening an exercise jumps straight to the first unfinished set (skipping + completed work/rest pairs) instead of snapping back to set 1. - Starting a workout on the iPhone now launches the Apple Watch app straight into the session via HealthKit (a one-time Health permission); the watch holds an `HKWorkoutSession` to stay active while you train and releases it when the diff --git a/Workouts Watch App/Views/ExerciseProgressView.swift b/Workouts Watch App/Views/ExerciseProgressView.swift index 0543b07..6cb8cdc 100644 --- a/Workouts Watch App/Views/ExerciseProgressView.swift +++ b/Workouts Watch App/Views/ExerciseProgressView.swift @@ -33,6 +33,7 @@ struct ExerciseProgressView: View { @State private var setCount: Int @State private var currentPage: Int @State private var showingCancelConfirm = false + @State private var didRestorePage = false init(doc: Binding, logID: String, onChange: @escaping () -> Void) { self._doc = doc @@ -54,6 +55,13 @@ struct ExerciseProgressView: View { /// Work₁, Rest₁, …, Workₙ ⇒ N sets + (N−1) rests = 2N − 1 pages. private var totalPages: Int { setCount * 2 - 1 } + /// The first unfinished set's work page (clamped to the last set). Resuming an + /// exercise opens here, skipping any completed work/rest pairs. + private var resumePage: Int { + let completed = min(max(0, log?.currentStateIndex ?? 0), setCount - 1) + return completed * 2 + } + private var detail: String { guard let log else { return "" } if LoadType(rawValue: log.loadType) == .duration { @@ -86,6 +94,24 @@ struct ExerciseProgressView: View { .onChange(of: currentPage) { _, newPage in recordProgress(for: newPage) } + .onAppear { + // Jump to the first unfinished set. A paged TabView can settle on page 0 on + // first layout, so re-assert once more after this run loop. + guard !didRestorePage else { return } + didRestorePage = true + jumpToResumePage() + Task { @MainActor in jumpToResumePage() } + } + } + + /// Move to the resume page without animation, only if we're not already there + /// (so a re-assert after a TabView snap-to-0 is a no-op in the common case). + private func jumpToResumePage() { + let target = resumePage + guard currentPage != target else { return } + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { currentPage = target } } @ViewBuilder @@ -140,17 +166,21 @@ struct ExerciseProgressView: View { /// Page index → completed-set count: Work₁(0)→0, Rest₁(1)→1, Work₂(2)→1, … — /// i.e. `(pageIndex + 1) / 2`. Reaching set N as *completed* only happens via Done. + /// + /// Progress is **monotonic**: completing a work phase advances the count (and + /// forwards it to the phone), but swiping back — or a transient TabView snap to + /// page 0 — never un-counts a finished set. private func recordProgress(for pageIndex: Int) { - let completedSets = min((pageIndex + 1) / 2, setCount) + let reached = min((pageIndex + 1) / 2, setCount) guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return } - guard completedSets != doc.logs[i].currentStateIndex else { return } + guard reached > doc.logs[i].currentStateIndex else { return } - doc.logs[i].currentStateIndex = completedSets - if completedSets >= setCount { + doc.logs[i].currentStateIndex = reached + if reached >= setCount { doc.logs[i].status = WorkoutStatus.completed.rawValue doc.logs[i].completed = true - } else if completedSets > 0 { + } else { doc.logs[i].status = WorkoutStatus.inProgress.rawValue doc.logs[i].completed = false }