feat(watch): add HealthKit workout tracking with authorization and live data
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>NSHealthShareUsageDescription</key>
|
||||
<string>SwimBuddy needs access to your health data to track your swim workouts.</string>
|
||||
<key>NSHealthUpdateUsageDescription</key>
|
||||
<string>SwimBuddy needs to save your swim workout data to Health.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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() {
|
||||
|
||||
@@ -4,11 +4,13 @@ module.exports = (config) => ({
|
||||
type: "watch",
|
||||
icon: "../../assets/icon.png",
|
||||
deploymentTarget: "10.0",
|
||||
frameworks: ["SwiftUI"],
|
||||
frameworks: ["SwiftUI", "HealthKit"],
|
||||
entitlements: {
|
||||
"com.apple.security.application-groups":
|
||||
config.ios?.entitlements?.[
|
||||
"com.apple.security.application-groups"
|
||||
] ?? [],
|
||||
"com.apple.developer.healthkit": true,
|
||||
"com.apple.developer.healthkit.access": [],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user