feat(watch): refactor active workout into paged TabView with overview and controls

This commit is contained in:
2026-08-09 17:53:39 +02:00
parent acdf66bd84
commit 0979af288d
3 changed files with 120 additions and 25 deletions
+12
View File
@@ -415,6 +415,9 @@
}
}
}
},
"End Workout" : {
},
"kcal" : {
"extractionState" : "manual",
@@ -838,6 +841,9 @@
}
}
}
},
"Pause" : {
},
"Recent" : {
"extractionState" : "manual",
@@ -966,6 +972,12 @@
}
}
}
},
"Rest %@" : {
},
"Resume" : {
},
"Saved Workouts" : {
"extractionState" : "manual",
+105 -25
View File
@@ -4,18 +4,22 @@ struct ActiveWorkoutView: View {
@ObservedObject var workoutManager: WorkoutManager
var body: some View {
VStack(spacing: 8) {
TabView {
if workoutManager.isResting {
RestView(workoutManager: workoutManager)
RestPage(workoutManager: workoutManager)
} else {
SwimView(workoutManager: workoutManager)
ActivePage(workoutManager: workoutManager)
}
WorkoutOverviewPage(workoutManager: workoutManager)
ControlsPage(workoutManager: workoutManager)
}
.padding()
.tabViewStyle(.verticalPage)
}
}
struct SwimView: View {
// MARK: - Page 1: Active / Rest
struct ActivePage: View {
@ObservedObject var workoutManager: WorkoutManager
var body: some View {
@@ -82,24 +86,6 @@ struct SwimView: View {
.foregroundColor(.green)
.transition(.opacity)
}
HStack(spacing: 12) {
Button {
workoutManager.completeLap()
} label: {
Text("Complete Lap")
.font(.headline)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.cyan)
}
Button("End") {
workoutManager.endWorkout()
}
.font(.caption)
.foregroundColor(.red)
}
}
@@ -110,7 +96,7 @@ struct SwimView: View {
}
}
struct RestView: View {
struct RestPage: View {
@ObservedObject var workoutManager: WorkoutManager
var body: some View {
@@ -141,8 +127,8 @@ struct RestView: View {
}
HStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.caption)
.foregroundColor(.orange)
.font(.caption)
Text("\(Int(workoutManager.activeCalories))kcal")
.font(.caption)
}
@@ -157,3 +143,97 @@ struct RestView: View {
return String(format: "%d:%02d", mins, secs)
}
}
// MARK: - Page 2: Workout Overview
struct WorkoutOverviewPage: View {
@ObservedObject var workoutManager: WorkoutManager
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 6) {
if workoutManager.isResting {
HStack {
Image(systemName: "pause.circle.fill")
.foregroundColor(.orange)
.font(.caption)
Text("Rest \(formatTime(workoutManager.restTimeRemaining))")
.font(.caption)
.foregroundColor(.orange)
}
}
if let workout = workoutManager.selectedWorkout {
ForEach(Array(workout.sets.enumerated()), id: \.offset) { i, set in
HStack {
Circle()
.fill(i < workoutManager.currentSetIndex ? .green :
i == workoutManager.currentSetIndex ? .cyan : .gray.opacity(0.4))
.frame(width: 8, height: 8)
Text("\(i + 1). \(set.stroke)")
.font(.caption)
.foregroundColor(i <= workoutManager.currentSetIndex ? .primary : .secondary)
Spacer()
Text("\(set.repCount)x\(set.distancePerRep)m")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
}
.padding(.top, 4)
}
}
private func formatTime(_ seconds: TimeInterval) -> String {
let mins = Int(seconds) / 60
let secs = Int(seconds) % 60
return String(format: "%d:%02d", mins, secs)
}
}
// MARK: - Page 3: Controls
struct ControlsPage: View {
@ObservedObject var workoutManager: WorkoutManager
var body: some View {
VStack(spacing: 16) {
Spacer()
Button {
if workoutManager.isPaused {
workoutManager.resumeWorkout()
} else {
workoutManager.pauseWorkout()
}
} label: {
VStack(spacing: 4) {
Image(systemName: workoutManager.isPaused ? "play.fill" : "pause.fill")
.font(.title2)
Text(workoutManager.isPaused ? "Resume" : "Pause")
.font(.caption)
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.orange)
Button {
workoutManager.endWorkout()
} label: {
VStack(spacing: 4) {
Image(systemName: "stop.fill")
.font(.title2)
Text("End Workout")
.font(.caption)
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.red)
Spacer()
}
}
}
+3
View File
@@ -40,6 +40,7 @@ class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate {
private var stationarySince: Date?
@Published var lapDetected = false
@Published var healthKitAuthorized = false
@Published var isPaused = false
override init() {
super.init()
@@ -170,6 +171,7 @@ class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate {
stopLapDetection()
stopHeartRateQuery()
stopCalorieQuery()
isPaused = true
WKInterfaceDevice.current().play(.notification)
}
@@ -178,6 +180,7 @@ class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate {
startLapDetection()
startHeartRateQuery()
startCalorieQuery()
isPaused = false
WKInterfaceDevice.current().play(.start)
}