diff --git a/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift b/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift index 86dec89..2739dcb 100644 --- a/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift +++ b/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift @@ -27,6 +27,8 @@ struct WorkoutLogListView: View { @State private var showingAddSheet = false @State private var showingEndOptions = false + @State private var showingActionMenu = false + @State private var pendingMenuAction: MenuAction? @State private var logToDelete: WorkoutLogDocument? @State private var addedLog: LogRoute? @State private var logToEdit: LogRoute? @@ -38,6 +40,11 @@ struct WorkoutLogListView: View { /// double-fire the way a value-based `navigationDestination(for:)` would. private struct LogRoute: Identifiable, Hashable { let id: String } + /// The overflow actions surfaced by the toolbar's "…" button. The chosen action is + /// stashed here and run from the sheet's `onDismiss`, so we never try to present a + /// second sheet/dialog while the menu sheet is still on screen. + private enum MenuAction { case addExercise, endWorkout } + init(workout: Workout) { self.workout = workout _doc = State(initialValue: WorkoutDocument(from: workout)) @@ -131,24 +138,27 @@ struct WorkoutLogListView: View { } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Menu { - Button { - showingAddSheet = true - } label: { - Label("Add Exercise", systemImage: "plus") - } - if !sortedLogs.isEmpty { - Button { - showingEndOptions = true - } label: { - Label("End Workout", systemImage: "flag.checkered") - } - } + Button { + showingActionMenu = true } label: { Image(systemName: "ellipsis.circle") } + .accessibilityLabel("Workout Options") } } + .sheet(isPresented: $showingActionMenu, onDismiss: runPendingMenuAction) { + WorkoutActionsSheet( + canEndWorkout: !sortedLogs.isEmpty, + onAddExercise: { + pendingMenuAction = .addExercise + showingActionMenu = false + }, + onEndWorkout: { + pendingMenuAction = .endWorkout + showingActionMenu = false + } + ) + } .sheet(isPresented: $showingAddSheet) { SplitExercisePickerSheet( split: split, @@ -187,6 +197,18 @@ struct WorkoutLogListView: View { } } + /// Run the action chosen in the overflow sheet, once that sheet has finished + /// dismissing — presenting these follow-on sheets/dialogs while the menu is still + /// up would collide, since SwiftUI supports only one sheet at a time. + private func runPendingMenuAction() { + switch pendingMenuAction { + case .addExercise: showingAddSheet = true + case .endWorkout: showingEndOptions = true + case .none: break + } + pendingMenuAction = nil + } + /// The paged run flow, fully wired into the live channel: it broadcasts this device's /// human transitions to the watch, follows the watch's, and marks this run as open inline /// (so the propped-phone mirror cover doesn't stack on top of it). @@ -342,6 +364,34 @@ struct WorkoutLogListView: View { } } +// MARK: - Workout Actions Sheet + +/// Compact, detented bottom sheet that replaces the toolbar's overflow `Menu`. Under +/// iOS 26 a toolbar `Menu` anchors to its button at the top of the screen; presenting +/// the same actions as a small sheet keeps them at the bottom within thumb reach, and +/// lets us pin the height via `presentationDetents`. +private struct WorkoutActionsSheet: View { + let canEndWorkout: Bool + let onAddExercise: () -> Void + let onEndWorkout: () -> Void + + var body: some View { + List { + Button(action: onAddExercise) { + Label("Add Exercise", systemImage: "plus") + } + if canEndWorkout { + Button(action: onEndWorkout) { + Label("End Workout", systemImage: "flag.checkered") + } + } + } + // Sized to fit just the row(s); bump these if the row metrics change. + .presentationDetents([.height(canEndWorkout ? 200 : 140)]) + .presentationDragIndicator(.visible) + } +} + // MARK: - Split Exercise Picker Sheet struct SplitExercisePickerSheet: View {