Make watch HIIT progress monotonic and resume to next unfinished set
Completing a work phase advances the set count to the iPhone, and a finished set is never un-counted — a transient paged-TabView snap to page 0 can no longer overwrite progress with 0. Reopening an exercise now jumps to the first unfinished set's work page (re-asserted after first layout) instead of starting back at set 1. Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<WorkoutDocument>, 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user