feat(watch): add HealthKit workout tracking with authorization and live data

This commit is contained in:
2026-08-09 17:45:03 +02:00
parent 2333ba7caa
commit acdf66bd84
3 changed files with 93 additions and 10 deletions
+84 -8
View File
@@ -5,7 +5,7 @@ import WatchKit
import WatchConnectivity
import CoreMotion
class WorkoutManager: ObservableObject {
class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate {
@Published var isWorkoutActive = false
@Published var savedWorkouts: [StoredWorkout] = []
@Published var completedWorkouts: [StoredWorkout] = []
@@ -39,9 +39,41 @@ class WorkoutManager: ObservableObject {
private var wasStationary = false
private var stationarySince: Date?
@Published var lapDetected = false
@Published var healthKitAuthorized = false
init() {
override init() {
super.init()
healthStore = HKHealthStore()
requestHealthKitAuthorization()
}
private func requestHealthKitAuthorization() {
guard let healthStore = healthStore else { return }
let typesToRead: Set<HKObjectType> = [
HKQuantityType.quantityType(forIdentifier: .heartRate)!,
HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.workoutType()
]
let typesToWrite: Set<HKSampleType> = [
HKObjectType.workoutType()
]
if healthStore.authorizationStatus(for: .workoutType()) == .sharingAuthorized {
DispatchQueue.main.async {
self.healthKitAuthorized = true
}
return
}
healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { [weak self] success, error in
if let error = error {
print("HealthKit authorization failed: \(error)")
}
DispatchQueue.main.async {
self?.healthKitAuthorized = success
print("HealthKit authorization: \(success)")
}
}
}
// MARK: - Data Persistence
@@ -106,6 +138,12 @@ class WorkoutManager: ObservableObject {
func startWorkout() {
guard selectedWorkout != nil else { return }
if !healthKitAuthorized {
print("HealthKit not authorized, requesting...")
requestHealthKitAuthorization()
return
}
isWorkoutActive = true
currentSetIndex = 0
currentRep = 0
@@ -116,12 +154,14 @@ class WorkoutManager: ObservableObject {
wasStationary = false
stationarySince = nil
startHealthKitWorkout()
startHealthKitWorkout(poolLength: selectedWorkout?.poolLength ?? 25)
startHeartRateQuery()
startCalorieQuery()
startTimer()
startLapDetection()
WKInterfaceDevice.current().enableWaterLock()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
WKInterfaceDevice.current().enableWaterLock()
}
WKInterfaceDevice.current().play(.start)
}
@@ -418,19 +458,26 @@ class WorkoutManager: ObservableObject {
// MARK: - HealthKit
private func startHealthKitWorkout() {
private func startHealthKitWorkout(poolLength: Double) {
guard let healthStore = healthStore else { return }
let config = HKWorkoutConfiguration()
config.activityType = .swimming
config.locationType = .indoor
config.swimmingLocationType = .pool
config.lapLength = HKQuantity(unit: .meter(), doubleValue: poolLength)
do {
activeWorkout = try HKWorkoutSession(healthStore: healthStore, configuration: config)
let workoutBuilder = HKWorkoutBuilder(healthStore: healthStore, configuration: config, device: nil)
let workoutBuilder = activeWorkout!.associatedWorkoutBuilder()
workoutBuilder.delegate = self
self.builder = workoutBuilder
activeWorkout?.startActivity(with: Date())
workoutBuilder.beginCollection(withStart: Date()) { _, _ in }
workoutBuilder.beginCollection(withStart: Date()) { success, error in
if let error = error {
print("Failed to begin collection: \(error)")
}
}
} catch {
print("Failed to start HealthKit workout: \(error)")
}
@@ -438,11 +485,40 @@ class WorkoutManager: ObservableObject {
private func endHealthKitWorkout() {
activeWorkout?.end()
builder?.endCollection(withEnd: Date()) { [weak self] _, _ in
builder?.endCollection(withEnd: Date()) { [weak self] success, error in
if let error = error {
print("Failed to end collection: \(error)")
}
self?.builder?.finishWorkout { _, _ in }
}
}
// MARK: - HKLiveWorkoutBuilderDelegate
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else { continue }
if let statistics = workoutBuilder.statistics(for: quantityType) {
DispatchQueue.main.async {
switch quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: .minute())
self.heartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
self.activeCalories = statistics.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? 0
default:
break
}
}
}
}
}
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
// Handle collected events if needed
}
// MARK: - Sync to Phone
private func syncCompletedWorkout() {