feat(watch): add lap detection via accelerometer and workout sync to phone
This commit is contained in:
@@ -24,44 +24,83 @@ struct SwimView: View {
|
||||
workoutManager.currentSetIndex < workout.sets.count {
|
||||
let currentSet = workout.sets[workoutManager.currentSetIndex]
|
||||
|
||||
Text("Set \(workoutManager.currentSetIndex + 1)/\(workout.sets.count)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
// Set/Rep info
|
||||
HStack {
|
||||
Text("Set \(workoutManager.currentSetIndex + 1)/\(workout.sets.count)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
Text("·")
|
||||
.foregroundColor(.secondary)
|
||||
Text("Rep \(workoutManager.currentRep + 1)/\(currentSet.repCount)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// Stroke name
|
||||
Text(currentSet.stroke)
|
||||
.font(.headline)
|
||||
.foregroundColor(.cyan)
|
||||
|
||||
Text("Rep \(workoutManager.currentRep + 1)/\(currentSet.repCount)")
|
||||
.font(.subheadline)
|
||||
|
||||
Text("\(currentSet.distancePerRep)m")
|
||||
// Current set distance
|
||||
Text("\(Int(workoutManager.currentSetDistanceSwum))/\(Int(workoutManager.currentSetTotalDistance))m")
|
||||
.font(.system(size: 36, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.cyan)
|
||||
}
|
||||
|
||||
Text(formatTime(workoutManager.elapsedSeconds))
|
||||
.font(.system(.title, design: .monospaced))
|
||||
// Total distance
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "figure.pool.swim")
|
||||
.font(.caption)
|
||||
.foregroundColor(.cyan)
|
||||
Text("\(Int(workoutManager.totalDistanceSwum))m total")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if workoutManager.heartRate > 0 {
|
||||
// Elapsed time
|
||||
Text(formatTime(workoutManager.elapsedSeconds))
|
||||
.font(.system(.title3, design: .monospaced))
|
||||
|
||||
// Calories + Heart rate
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(.red)
|
||||
Image(systemName: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
.font(.caption)
|
||||
Text("\(Int(workoutManager.heartRate))")
|
||||
Text("\(Int(workoutManager.estimatedCalories))")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
if workoutManager.heartRate > 0 {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(.red)
|
||||
.font(.caption)
|
||||
Text("\(Int(workoutManager.heartRate))")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
workoutManager.completeLap()
|
||||
} label: {
|
||||
Text("Complete Lap")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
// Lap detection indicator
|
||||
if workoutManager.lapDetected {
|
||||
Text("Lap detected!")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
workoutManager.completeLap()
|
||||
} label: {
|
||||
Text("Complete Lap")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.cyan)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.cyan)
|
||||
|
||||
Button("End") {
|
||||
workoutManager.endWorkout()
|
||||
@@ -98,6 +137,25 @@ struct RestView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// Show stats during rest too
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "figure.pool.swim")
|
||||
.font(.caption)
|
||||
.foregroundColor(.cyan)
|
||||
Text("\(Int(workoutManager.totalDistanceSwum))m")
|
||||
.font(.caption)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "flame.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
Text("\(Int(workoutManager.estimatedCalories))kcal")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WorkoutDetailView: View {
|
||||
@ObservedObject var workoutManager: WorkoutManager
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if let workout = workoutManager.selectedWorkout {
|
||||
Section {
|
||||
VStack(spacing: 8) {
|
||||
Text(workout.name ?? "Workout")
|
||||
.font(.headline)
|
||||
|
||||
Text("\(Int(workout.poolLength))m pool")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("\(workout.sets.reduce(0) { $0 + $1.distancePerRep * $1.repCount })m total")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
Section("Sets") {
|
||||
ForEach(Array(workout.sets.enumerated()), id: \.offset) { i, set in
|
||||
HStack {
|
||||
Text("\(i + 1). \(set.stroke)")
|
||||
.font(.body)
|
||||
Spacer()
|
||||
Text("\(set.repCount)x\(set.distancePerRep)m")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
workoutManager.startWorkout()
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "play.fill")
|
||||
Text("Start")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.cyan)
|
||||
|
||||
Button("Back") {
|
||||
workoutManager.selectedWorkout = nil
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.automatic)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user