import Combine import Foundation import HealthKit @MainActor class HealthKitService: ObservableObject { static let shared = HealthKitService() private let healthStore = HKHealthStore() private var currentWorkoutStartDate: Date? private var currentWorkoutSessionId: UUID? @Published var isAuthorized = false @Published var isEnabled = false private let userDefaults = UserDefaults.standard private let isEnabledKey = "healthKitEnabled" private let workoutStartDateKey = "healthKitWorkoutStartDate" private let workoutSessionIdKey = "healthKitWorkoutSessionId" private init() { loadSettings() restoreActiveWorkout() } func loadSettings() { isEnabled = userDefaults.bool(forKey: isEnabledKey) if HKHealthStore.isHealthDataAvailable() { checkAuthorization() } } /// Restore active workout state private func restoreActiveWorkout() { if let startDate = userDefaults.object(forKey: workoutStartDateKey) as? Date, let sessionIdString = userDefaults.string(forKey: workoutSessionIdKey), let sessionId = UUID(uuidString: sessionIdString) { currentWorkoutStartDate = startDate currentWorkoutSessionId = sessionId print("HealthKit: Restored active workout from \(startDate)") } } /// Persist active workout state private func persistActiveWorkout() { if let startDate = currentWorkoutStartDate, let sessionId = currentWorkoutSessionId { userDefaults.set(startDate, forKey: workoutStartDateKey) userDefaults.set(sessionId.uuidString, forKey: workoutSessionIdKey) } else { userDefaults.removeObject(forKey: workoutStartDateKey) userDefaults.removeObject(forKey: workoutSessionIdKey) } } /// Verify and restore health integration func verifyAndRestoreIntegration() async { guard isEnabled else { return } guard HKHealthStore.isHealthDataAvailable() else { print("HealthKit: Device does not support HealthKit") return } checkAuthorization() if !isAuthorized { print( "HealthKit: Integration was enabled but authorization lost, attempting to restore..." ) do { try await requestAuthorization() print("HealthKit: Authorization restored successfully") } catch { print("HealthKit: Failed to restore authorization: \(error.localizedDescription)") } } else { print("HealthKit: Integration verified - authorization is valid") } if hasActiveWorkout() { print( "HealthKit: Active workout restored - started at \(currentWorkoutStartDate!)" ) } } func setEnabled(_ enabled: Bool) { isEnabled = enabled userDefaults.set(enabled, forKey: isEnabledKey) } func requestAuthorization() async throws { guard HKHealthStore.isHealthDataAvailable() else { throw HealthKitError.notAvailable } let workoutType = HKObjectType.workoutType() let energyBurnedType = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)! let typesToShare: Set = [ workoutType, energyBurnedType, ] let typesToRead: Set = [ workoutType ] try await healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) self.isAuthorized = true } private func checkAuthorization() { let workoutType = HKObjectType.workoutType() let status = healthStore.authorizationStatus(for: workoutType) isAuthorized = (status == .sharingAuthorized) } func startWorkout(startDate: Date, sessionId: UUID) async throws { guard isEnabled && isAuthorized else { return } guard HKHealthStore.isHealthDataAvailable() else { throw HealthKitError.notAvailable } currentWorkoutStartDate = startDate currentWorkoutSessionId = sessionId persistActiveWorkout() print("HealthKit: Started workout for session \(sessionId)") } func endWorkout(endDate: Date) async throws { guard isEnabled && isAuthorized else { return } guard let startDate = currentWorkoutStartDate else { return } guard HKHealthStore.isHealthDataAvailable() else { throw HealthKitError.notAvailable } let duration = endDate.timeIntervalSince(startDate) let calories = estimateCalories(durationInMinutes: duration / 60.0) let energyBurned = HKQuantity(unit: .kilocalorie(), doubleValue: calories) let workoutConfiguration = HKWorkoutConfiguration() workoutConfiguration.activityType = .climbing workoutConfiguration.locationType = .indoor let builder = HKWorkoutBuilder( healthStore: healthStore, configuration: workoutConfiguration, device: .local() ) do { try await builder.beginCollection(at: startDate) let energyBurnedType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)! let energySample = HKQuantitySample( type: energyBurnedType, quantity: energyBurned, start: startDate, end: endDate ) try await builder.addSamples([energySample]) try await builder.addMetadata([HKMetadataKeyIndoorWorkout: true]) try await builder.endCollection(at: endDate) let workout = try await builder.finishWorkout() print( "HealthKit: Workout saved successfully with id: \(workout?.uuid.uuidString ?? "unknown")" ) currentWorkoutStartDate = nil currentWorkoutSessionId = nil persistActiveWorkout() } catch { print("HealthKit: Failed to save workout: \(error.localizedDescription)") currentWorkoutStartDate = nil currentWorkoutSessionId = nil persistActiveWorkout() throw HealthKitError.workoutSaveFailed } } func cancelWorkout() { currentWorkoutStartDate = nil currentWorkoutSessionId = nil persistActiveWorkout() print("HealthKit: Workout cancelled") } func hasActiveWorkout() -> Bool { return currentWorkoutStartDate != nil } private func estimateCalories(durationInMinutes: Double) -> Double { let caloriesPerMinute = 8.0 return durationInMinutes * caloriesPerMinute } } enum HealthKitError: LocalizedError { case notAvailable case notAuthorized case workoutStartFailed case workoutSaveFailed var errorDescription: String? { switch self { case .notAvailable: return "HealthKit is not available on this device" case .notAuthorized: return "HealthKit authorization not granted" case .workoutStartFailed: return "Failed to start HealthKit workout" case .workoutSaveFailed: return "Failed to save workout to HealthKit" } } }