feat(watch): add workout summary screen with HealthKit data
This commit is contained in:
@@ -15,8 +15,10 @@ class WorkoutManager: ObservableObject {
|
||||
@Published var currentLapDistance = 0
|
||||
@Published var elapsedSeconds: TimeInterval = 0
|
||||
@Published var heartRate: Double = 0
|
||||
@Published var activeCalories: Double = 0
|
||||
@Published var isResting = false
|
||||
@Published var restTimeRemaining: TimeInterval = 0
|
||||
@Published var workoutSummary: WorkoutSummary?
|
||||
|
||||
private var workoutTimer: Timer?
|
||||
private var restTimer: Timer?
|
||||
@@ -26,6 +28,10 @@ class WorkoutManager: ObservableObject {
|
||||
private var lapStartTime: Date?
|
||||
private var completedLaps: [CompletedLap] = []
|
||||
|
||||
// Heart rate & calorie queries
|
||||
private var heartRateQuery: HKObserverQuery?
|
||||
private var calorieQuery: HKObserverQuery?
|
||||
|
||||
// Lap detection
|
||||
private let motionManager = CMMotionManager()
|
||||
private var lapDetectionTimer: Timer?
|
||||
@@ -91,6 +97,10 @@ class WorkoutManager: ObservableObject {
|
||||
selectedWorkout = workout
|
||||
}
|
||||
|
||||
func dismissSummary() {
|
||||
workoutSummary = nil
|
||||
}
|
||||
|
||||
// MARK: - Workout Control
|
||||
|
||||
func startWorkout() {
|
||||
@@ -100,11 +110,15 @@ class WorkoutManager: ObservableObject {
|
||||
currentSetIndex = 0
|
||||
currentRep = 0
|
||||
elapsedSeconds = 0
|
||||
activeCalories = 0
|
||||
heartRate = 0
|
||||
completedLaps = []
|
||||
wasStationary = false
|
||||
stationarySince = nil
|
||||
|
||||
startHealthKitWorkout()
|
||||
startHeartRateQuery()
|
||||
startCalorieQuery()
|
||||
startTimer()
|
||||
startLapDetection()
|
||||
WKInterfaceDevice.current().enableWaterLock()
|
||||
@@ -114,12 +128,16 @@ class WorkoutManager: ObservableObject {
|
||||
func pauseWorkout() {
|
||||
workoutTimer?.invalidate()
|
||||
stopLapDetection()
|
||||
stopHeartRateQuery()
|
||||
stopCalorieQuery()
|
||||
WKInterfaceDevice.current().play(.notification)
|
||||
}
|
||||
|
||||
func resumeWorkout() {
|
||||
startTimer()
|
||||
startLapDetection()
|
||||
startHeartRateQuery()
|
||||
startCalorieQuery()
|
||||
WKInterfaceDevice.current().play(.start)
|
||||
}
|
||||
|
||||
@@ -129,11 +147,27 @@ class WorkoutManager: ObservableObject {
|
||||
isWorkoutActive = false
|
||||
|
||||
stopLapDetection()
|
||||
stopHeartRateQuery()
|
||||
stopCalorieQuery()
|
||||
endHealthKitWorkout()
|
||||
|
||||
createWorkoutSummary()
|
||||
syncCompletedWorkout()
|
||||
WKInterfaceDevice.current().play(.success)
|
||||
}
|
||||
|
||||
// MARK: - Workout Summary
|
||||
|
||||
private func createWorkoutSummary() {
|
||||
let summary = WorkoutSummary(
|
||||
duration: elapsedSeconds,
|
||||
totalDistance: Int(totalDistanceSwum),
|
||||
calories: Int(activeCalories),
|
||||
laps: completedLaps
|
||||
)
|
||||
workoutSummary = summary
|
||||
}
|
||||
|
||||
// MARK: - Lap Tracking
|
||||
|
||||
func completeLap() {
|
||||
@@ -233,8 +267,87 @@ class WorkoutManager: ObservableObject {
|
||||
return Double(set.distancePerRep) * Double(set.repCount)
|
||||
}
|
||||
|
||||
var estimatedCalories: Double {
|
||||
return totalDistanceSwum * 0.07
|
||||
// MARK: - Heart Rate Query
|
||||
|
||||
private func startHeartRateQuery() {
|
||||
guard let healthStore = healthStore else { return }
|
||||
let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
|
||||
|
||||
heartRateQuery = HKObserverQuery(sampleType: heartRateType, predicate: nil) { [weak self] _, completionHandler, _ in
|
||||
self?.fetchLatestHeartRate()
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
if let query = heartRateQuery {
|
||||
healthStore.execute(query)
|
||||
healthStore.enableBackgroundDelivery(for: heartRateType, frequency: .immediate) { _, _, _ in }
|
||||
}
|
||||
}
|
||||
|
||||
private func stopHeartRateQuery() {
|
||||
if let query = heartRateQuery {
|
||||
healthStore?.stop(query)
|
||||
heartRateQuery = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchLatestHeartRate() {
|
||||
guard let healthStore = healthStore else { return }
|
||||
let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
|
||||
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
|
||||
|
||||
let query = HKSampleQuery(sampleType: heartRateType, predicate: nil, limit: 1, sortDescriptors: [sortDescriptor]) { [weak self] _, samples, _ in
|
||||
guard let sample = samples?.first as? HKQuantitySample else { return }
|
||||
let heartRateUnit = HKUnit.count().unitDivided(by: .minute())
|
||||
let value = sample.quantity.doubleValue(for: heartRateUnit)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.heartRate = value
|
||||
}
|
||||
}
|
||||
healthStore.execute(query)
|
||||
}
|
||||
|
||||
// MARK: - Calorie Query
|
||||
|
||||
private func startCalorieQuery() {
|
||||
guard let healthStore = healthStore else { return }
|
||||
let calorieType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
|
||||
|
||||
calorieQuery = HKObserverQuery(sampleType: calorieType, predicate: nil) { [weak self] _, completionHandler, _ in
|
||||
self?.fetchActiveCalories()
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
if let query = calorieQuery {
|
||||
healthStore.execute(query)
|
||||
healthStore.enableBackgroundDelivery(for: calorieType, frequency: .immediate) { _, _, _ in }
|
||||
}
|
||||
}
|
||||
|
||||
private func stopCalorieQuery() {
|
||||
if let query = calorieQuery {
|
||||
healthStore?.stop(query)
|
||||
calorieQuery = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchActiveCalories() {
|
||||
guard let healthStore = healthStore else { return }
|
||||
let calorieType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
|
||||
let now = Date()
|
||||
|
||||
let predicate = HKQuery.predicateForSamples(withStart: now.addingTimeInterval(-elapsedSeconds), end: now, options: .strictStartDate)
|
||||
|
||||
let query = HKStatisticsQuery(quantityType: calorieType, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] _, result, _ in
|
||||
guard let sum = result?.sumQuantity() else { return }
|
||||
let calories = sum.doubleValue(for: HKUnit.kilocalorie())
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.activeCalories = calories
|
||||
}
|
||||
}
|
||||
healthStore.execute(query)
|
||||
}
|
||||
|
||||
// MARK: - Lap Detection (Accelerometer)
|
||||
@@ -349,7 +462,7 @@ class WorkoutManager: ObservableObject {
|
||||
},
|
||||
"duration": elapsedSeconds,
|
||||
"totalDistance": Int(totalDistanceSwum),
|
||||
"calories": Int(estimatedCalories),
|
||||
"calories": Int(activeCalories),
|
||||
]
|
||||
|
||||
var completed = completedWorkouts
|
||||
@@ -393,3 +506,10 @@ struct CompletedLap {
|
||||
let duration: TimeInterval
|
||||
let heartRate: Int?
|
||||
}
|
||||
|
||||
struct WorkoutSummary {
|
||||
let duration: TimeInterval
|
||||
let totalDistance: Int
|
||||
let calories: Int
|
||||
let laps: [CompletedLap]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user