End the watch session on Discard, plus start-flow UX tweaks
Watch-side follow-through for the End Workout flow: - The phone now pushes an authoritative set (in-progress, not-started, and completed within 24h) instead of the 25 most-recent workouts, and the watch prunes any workout absent from it. So a Discard/Delete (or a completed run aging out) drops off the watch, empties its active list, and ends the HKWorkoutSession — fixing the persistent wrist-raise re-foregrounding. The watch never originates a workout, so pruning can't lose local data; the 24h grace keeps a just-finished run on screen. The gate pops if the run you're viewing is pruned. UX tweaks: - The in-workout ⋯ is now a pull-down Menu (Add Exercise / End Workout) rather than an action sheet. - Starting a split while another workout is still active now prompts to end the current one(s) — keeping their progress — or run in parallel. Wired into both start paths (the split picker and "Start This Split"), via a shared WorkoutDocument.endKeepingProgress() helper. Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
This commit is contained in:
@@ -24,6 +24,15 @@ struct ExerciseListView: View {
|
||||
@State private var pendingWorkoutID: String? = nil
|
||||
@State private var resolvedWorkout: Workout? = nil
|
||||
|
||||
@Query(sort: \Workout.start, order: .reverse)
|
||||
private var workouts: [Workout]
|
||||
|
||||
@State private var showingActivePrompt = false
|
||||
|
||||
private var activeWorkouts: [Workout] {
|
||||
workouts.filter { $0.status == .inProgress || $0.status == .notStarted }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
let sortedExercises = split.exercisesArray
|
||||
@@ -67,7 +76,7 @@ struct ExerciseListView: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button("Start This Split") {
|
||||
startWorkout()
|
||||
confirmAndStart()
|
||||
}
|
||||
.disabled(split.exercisesArray.isEmpty)
|
||||
}
|
||||
@@ -108,10 +117,53 @@ struct ExerciseListView: View {
|
||||
} message: { item in
|
||||
Text("Remove \"\(item.name)\" from this split?")
|
||||
}
|
||||
.confirmationDialog(
|
||||
activePromptTitle,
|
||||
isPresented: $showingActivePrompt,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("End Current & Start New") { endActiveThenStart() }
|
||||
Button("Start in Parallel") { start() }
|
||||
Button("Cancel", role: .cancel) { showingActivePrompt = false }
|
||||
} message: {
|
||||
Text(activePromptMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private var activePromptTitle: String {
|
||||
activeWorkouts.count == 1 ? "Workout in Progress" : "\(activeWorkouts.count) Workouts in Progress"
|
||||
}
|
||||
|
||||
private var activePromptMessage: String {
|
||||
let n = activeWorkouts.count
|
||||
let those = n == 1 ? "it" : "them"
|
||||
return "You already have \(n == 1 ? "a workout" : "\(n) workouts") going. End \(those) first, or run this one alongside."
|
||||
}
|
||||
|
||||
/// Prompt before starting if other workouts are still going; otherwise start straight away.
|
||||
private func confirmAndStart() {
|
||||
if activeWorkouts.isEmpty {
|
||||
start()
|
||||
} else {
|
||||
showingActivePrompt = true
|
||||
}
|
||||
}
|
||||
|
||||
/// End every in-flight workout (keeping its progress), then start this split.
|
||||
private func endActiveThenStart() {
|
||||
let toEnd = activeWorkouts.map { WorkoutDocument(from: $0) }
|
||||
showingActivePrompt = false
|
||||
Task {
|
||||
for var doc in toEnd {
|
||||
doc.endKeepingProgress()
|
||||
await sync.save(workout: doc)
|
||||
}
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
private func pollForWorkout(id: String) {
|
||||
Task {
|
||||
// Give the file→observer→cache loop a moment to complete (typically < 1 s).
|
||||
@@ -141,8 +193,8 @@ struct ExerciseListView: View {
|
||||
Task { await sync.save(split: doc) }
|
||||
}
|
||||
|
||||
private func startWorkout() {
|
||||
let start = Date()
|
||||
private func start() {
|
||||
let startDate = Date()
|
||||
let logs = split.exercisesArray.enumerated().map { i, ex in
|
||||
WorkoutLogDocument(
|
||||
id: ULID.make(), exerciseName: ex.name, order: i,
|
||||
@@ -150,7 +202,7 @@ struct ExerciseListView: View {
|
||||
loadType: ex.loadType, durationSeconds: ex.durationTotalSeconds,
|
||||
currentStateIndex: 0, completed: false,
|
||||
status: WorkoutStatus.notStarted.rawValue,
|
||||
notes: nil, date: start
|
||||
notes: nil, date: startDate
|
||||
)
|
||||
}
|
||||
let doc = WorkoutDocument(
|
||||
@@ -158,11 +210,11 @@ struct ExerciseListView: View {
|
||||
id: ULID.make(),
|
||||
splitID: split.id,
|
||||
splitName: split.name,
|
||||
start: start,
|
||||
start: startDate,
|
||||
end: nil,
|
||||
status: WorkoutStatus.notStarted.rawValue,
|
||||
createdAt: start,
|
||||
updatedAt: start,
|
||||
createdAt: startDate,
|
||||
updatedAt: startDate,
|
||||
logs: logs
|
||||
)
|
||||
Task {
|
||||
|
||||
Reference in New Issue
Block a user