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:
@@ -114,12 +114,23 @@ struct SplitPickerSheet: View {
|
||||
@Query(sort: [SortDescriptor(\Split.order), SortDescriptor(\Split.name)])
|
||||
private var splits: [Split]
|
||||
|
||||
@Query(sort: \Workout.start, order: .reverse)
|
||||
private var workouts: [Workout]
|
||||
|
||||
/// Set when the user picks a split while other workouts are still going — drives the
|
||||
/// "end the current one(s) or run in parallel?" prompt.
|
||||
@State private var splitAwaitingConfirmation: Split?
|
||||
|
||||
private var activeWorkouts: [Workout] {
|
||||
workouts.filter { $0.status == .inProgress || $0.status == .notStarted }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(splits) { split in
|
||||
Button {
|
||||
startWorkout(with: split)
|
||||
confirmAndStart(with: split)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: split.systemImage)
|
||||
@@ -141,11 +152,58 @@ struct SplitPickerSheet: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
activePromptTitle,
|
||||
isPresented: Binding(
|
||||
get: { splitAwaitingConfirmation != nil },
|
||||
set: { if !$0 { splitAwaitingConfirmation = nil } }
|
||||
),
|
||||
titleVisibility: .visible,
|
||||
presenting: splitAwaitingConfirmation
|
||||
) { split in
|
||||
Button("End Current & Start New") { endActiveThenStart(with: split) }
|
||||
Button("Start in Parallel") { start(with: split) }
|
||||
Button("Cancel", role: .cancel) { splitAwaitingConfirmation = nil }
|
||||
} message: { _ in
|
||||
Text(activePromptMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startWorkout(with split: Split) {
|
||||
let start = Date()
|
||||
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(with split: Split) {
|
||||
if activeWorkouts.isEmpty {
|
||||
start(with: split)
|
||||
} else {
|
||||
splitAwaitingConfirmation = split
|
||||
}
|
||||
}
|
||||
|
||||
/// End every in-flight workout (keeping its progress), then start the picked split.
|
||||
private func endActiveThenStart(with split: Split) {
|
||||
let toEnd = activeWorkouts.map { WorkoutDocument(from: $0) }
|
||||
splitAwaitingConfirmation = nil
|
||||
Task {
|
||||
for var doc in toEnd {
|
||||
doc.endKeepingProgress()
|
||||
await sync.save(workout: doc)
|
||||
}
|
||||
}
|
||||
start(with: split)
|
||||
}
|
||||
|
||||
private func start(with split: Split) {
|
||||
let startDate = Date()
|
||||
let logs = split.exercisesArray.enumerated().map { index, exercise in
|
||||
WorkoutLogDocument(
|
||||
id: ULID.make(),
|
||||
@@ -160,7 +218,7 @@ struct SplitPickerSheet: View {
|
||||
completed: false,
|
||||
status: WorkoutStatus.notStarted.rawValue,
|
||||
notes: nil,
|
||||
date: start
|
||||
date: startDate
|
||||
)
|
||||
}
|
||||
|
||||
@@ -170,11 +228,11 @@ struct SplitPickerSheet: 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
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user