Present the workout overflow actions as a bottom sheet

Convert the ⋯ toolbar Menu in WorkoutLogListView to a Button that presents a small detented sheet, so the Add Exercise / End Workout actions appear at the bottom within thumb reach instead of anchoring to the top under iOS 26. The tapped action is stashed and run from the sheet's onDismiss, since SwiftUI presents only one sheet at a time.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
This commit is contained in:
2026-06-22 22:00:30 -04:00
parent 7400094eda
commit 59490d3195
@@ -27,6 +27,8 @@ struct WorkoutLogListView: View {
@State private var showingAddSheet = false @State private var showingAddSheet = false
@State private var showingEndOptions = false @State private var showingEndOptions = false
@State private var showingActionMenu = false
@State private var pendingMenuAction: MenuAction?
@State private var logToDelete: WorkoutLogDocument? @State private var logToDelete: WorkoutLogDocument?
@State private var addedLog: LogRoute? @State private var addedLog: LogRoute?
@State private var logToEdit: LogRoute? @State private var logToEdit: LogRoute?
@@ -38,6 +40,11 @@ struct WorkoutLogListView: View {
/// double-fire the way a value-based `navigationDestination(for:)` would. /// double-fire the way a value-based `navigationDestination(for:)` would.
private struct LogRoute: Identifiable, Hashable { let id: String } 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) { init(workout: Workout) {
self.workout = workout self.workout = workout
_doc = State(initialValue: WorkoutDocument(from: workout)) _doc = State(initialValue: WorkoutDocument(from: workout))
@@ -131,24 +138,27 @@ struct WorkoutLogListView: View {
} }
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Menu { Button {
Button { showingActionMenu = true
showingAddSheet = true
} label: {
Label("Add Exercise", systemImage: "plus")
}
if !sortedLogs.isEmpty {
Button {
showingEndOptions = true
} label: {
Label("End Workout", systemImage: "flag.checkered")
}
}
} label: { } label: {
Image(systemName: "ellipsis.circle") 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) { .sheet(isPresented: $showingAddSheet) {
SplitExercisePickerSheet( SplitExercisePickerSheet(
split: split, 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 /// 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 /// 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). /// (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 // MARK: - Split Exercise Picker Sheet
struct SplitExercisePickerSheet: View { struct SplitExercisePickerSheet: View {