diff --git a/targets/watch/Info.plist b/targets/watch/Info.plist
index f683276..f095216 100644
--- a/targets/watch/Info.plist
+++ b/targets/watch/Info.plist
@@ -1,5 +1,10 @@
-
+
+ NSHealthShareUsageDescription
+ SwimBuddy needs access to your health data to track your swim workouts.
+ NSHealthUpdateUsageDescription
+ SwimBuddy needs to save your swim workout data to Health.
+
\ No newline at end of file
diff --git a/targets/watch/WorkoutManager.swift b/targets/watch/WorkoutManager.swift
index 997f509..00da603 100644
--- a/targets/watch/WorkoutManager.swift
+++ b/targets/watch/WorkoutManager.swift
@@ -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 = [
+ HKQuantityType.quantityType(forIdentifier: .heartRate)!,
+ HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
+ HKObjectType.workoutType()
+ ]
+ let typesToWrite: Set = [
+ 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) {
+ 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() {
diff --git a/targets/watch/expo-target.config.js b/targets/watch/expo-target.config.js
index b16c9e0..a939029 100644
--- a/targets/watch/expo-target.config.js
+++ b/targets/watch/expo-target.config.js
@@ -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": [],
},
});