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" : { "kcal" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -838,6 +841,9 @@
} }
} }
} }
},
"Pause" : {
}, },
"Recent" : { "Recent" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -966,6 +972,12 @@
} }
} }
} }
},
"Rest %@" : {
},
"Resume" : {
}, },
"Saved Workouts" : { "Saved Workouts" : {
"extractionState" : "manual", "extractionState" : "manual",
+105 -25
View File
@@ -4,18 +4,22 @@ struct ActiveWorkoutView: View {
@ObservedObject var workoutManager: WorkoutManager @ObservedObject var workoutManager: WorkoutManager
var body: some View { var body: some View {
VStack(spacing: 8) { TabView {
if workoutManager.isResting { if workoutManager.isResting {
RestView(workoutManager: workoutManager) RestPage(workoutManager: workoutManager)
} else { } 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 @ObservedObject var workoutManager: WorkoutManager
var body: some View { var body: some View {
@@ -82,24 +86,6 @@ struct SwimView: View {
.foregroundColor(.green) .foregroundColor(.green)
.transition(.opacity) .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 @ObservedObject var workoutManager: WorkoutManager
var body: some View { var body: some View {
@@ -141,8 +127,8 @@ struct RestView: View {
} }
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "flame.fill") Image(systemName: "flame.fill")
.font(.caption)
.foregroundColor(.orange) .foregroundColor(.orange)
.font(.caption)
Text("\(Int(workoutManager.activeCalories))kcal") Text("\(Int(workoutManager.activeCalories))kcal")
.font(.caption) .font(.caption)
} }
@@ -157,3 +143,97 @@ struct RestView: View {
return String(format: "%d:%02d", mins, secs) 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? private var stationarySince: Date?
@Published var lapDetected = false @Published var lapDetected = false
@Published var healthKitAuthorized = false @Published var healthKitAuthorized = false
@Published var isPaused = false
override init() { override init() {
super.init() super.init()
@@ -170,6 +171,7 @@ class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate {
stopLapDetection() stopLapDetection()
stopHeartRateQuery() stopHeartRateQuery()
stopCalorieQuery() stopCalorieQuery()
isPaused = true
WKInterfaceDevice.current().play(.notification) WKInterfaceDevice.current().play(.notification)
} }
@@ -178,6 +180,7 @@ class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate {
startLapDetection() startLapDetection()
startHeartRateQuery() startHeartRateQuery()
startCalorieQuery() startCalorieQuery()
isPaused = false
WKInterfaceDevice.current().play(.start) WKInterfaceDevice.current().play(.start)
} }