Open new workouts directly and improve accessibility

Starting a workout — from the split picker or a split's exercise list —
now drops straight into its log screen once the cache catches up, via a
shared StartedWorkoutNavigator. Adds VoiceOver labels/values to the log
checkboxes and the settings button, a color-independent numbered legend
and spoken summary to the heart-rate-zone bar, and Dynamic Type scaling
to the run-flow badges and timer.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
2026-07-08 07:59:30 -04:00
parent 1b399ee7ba
commit e04eb83e70
7 changed files with 158 additions and 43 deletions
@@ -13,7 +13,6 @@ import SwiftData
struct ExerciseListView: View {
@Environment(SyncEngine.self) private var sync
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
// Resolve the split by id, not a captured entity: editing a seed's exercise from
@@ -26,9 +25,9 @@ struct ExerciseListView: View {
@State private var itemToEdit: Exercise? = nil
@State private var itemToDelete: Exercise? = nil
/// ID of the just-created workout; drives programmatic navigation once the
/// cache observer delivers the entity a beat after the file write.
/// cache observer delivers the entity a beat after the file write (see
/// `navigatesToStartedWorkout`).
@State private var pendingWorkoutID: String? = nil
@State private var resolvedWorkout: Workout? = nil
@Query(sort: \Workout.start, order: .reverse)
private var workouts: [Workout]
@@ -58,15 +57,8 @@ struct ExerciseListView: View {
.task { dismiss() }
}
}
// Navigate to the workout log once the entity appears in the cache.
.navigationDestination(item: $resolvedWorkout) { workout in
WorkoutLogListView(workout: workout)
}
// Poll for the entity after we write the document.
.onChange(of: pendingWorkoutID) { _, id in
guard let id else { return }
pollForWorkout(id: id)
}
// Navigate into the workout's log screen once the entity appears in the cache.
.navigatesToStartedWorkout(pendingWorkoutID: $pendingWorkoutID)
}
@ViewBuilder
@@ -192,22 +184,6 @@ struct ExerciseListView: View {
start()
}
private func pollForWorkout(id: String) {
Task {
// Give the fileobservercache loop a moment to complete (typically < 1 s).
for _ in 0..<20 {
try? await Task.sleep(for: .milliseconds(150))
if let workout = CacheMapper.fetchWorkout(id: id, in: modelContext) {
resolvedWorkout = workout
pendingWorkoutID = nil
return
}
}
// If still not available after ~3 s, clear the pending ID silently.
pendingWorkoutID = nil
}
}
private func moveExercises(from source: IndexSet, to destination: Int) {
guard let split else { return }
var exercises = split.exercisesArray