// // HealthKitManager.swift // Feels // // HealthKit State of Mind API integration for syncing mood data with Apple Health // import Foundation import HealthKit @MainActor class HealthKitManager: ObservableObject { static let shared = HealthKitManager() private let healthStore = HKHealthStore() @Published var isAuthorized = false @Published var authorizationError: Error? // State of Mind sample type private var stateOfMindType: HKSampleType? { HKSampleType.stateOfMindType() } // MARK: - Authorization var isHealthKitAvailable: Bool { HKHealthStore.isHealthDataAvailable() } func requestAuthorization() async throws { guard isHealthKitAvailable else { throw HealthKitError.notAvailable } guard let stateOfMindType = stateOfMindType else { throw HealthKitError.typeNotAvailable } let typesToShare: Set = [stateOfMindType] let typesToRead: Set = [stateOfMindType] try await healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) // Check authorization status let status = healthStore.authorizationStatus(for: stateOfMindType) isAuthorized = status == .sharingAuthorized } func checkAuthorizationStatus() -> HKAuthorizationStatus { guard let stateOfMindType = stateOfMindType else { return .notDetermined } return healthStore.authorizationStatus(for: stateOfMindType) } // MARK: - Save Mood to HealthKit func saveMood(_ mood: Mood, for date: Date, note: String? = nil) async throws { guard isHealthKitAvailable else { throw HealthKitError.notAvailable } guard checkAuthorizationStatus() == .sharingAuthorized else { throw HealthKitError.notAuthorized } // Convert Feels mood to HealthKit valence (-1 to 1 scale) let valence = moodToValence(mood) // Create State of Mind sample let stateOfMind = HKStateOfMind( date: date, kind: .dailyMood, valence: valence, labels: labelsForMood(mood), associations: [.currentEvents] ) try await healthStore.save(stateOfMind) } // MARK: - Read Mood from HealthKit func fetchMoods(from startDate: Date, to endDate: Date) async throws -> [HKStateOfMind] { guard isHealthKitAvailable else { throw HealthKitError.notAvailable } guard let stateOfMindType = stateOfMindType else { throw HealthKitError.typeNotAvailable } let predicate = HKQuery.predicateForSamples( withStart: startDate, end: endDate, options: .strictStartDate ) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false) return try await withCheckedThrowingContinuation { continuation in let query = HKSampleQuery( sampleType: stateOfMindType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor] ) { _, samples, error in if let error = error { continuation.resume(throwing: error) return } let stateOfMindSamples = samples?.compactMap { $0 as? HKStateOfMind } ?? [] continuation.resume(returning: stateOfMindSamples) } healthStore.execute(query) } } // MARK: - Conversion Helpers /// Convert Feels Mood to HealthKit valence (-1 to 1) private func moodToValence(_ mood: Mood) -> Double { switch mood { case .horrible: return -1.0 case .bad: return -0.5 case .average: return 0.0 case .good: return 0.5 case .great: return 1.0 case .missing, .placeholder: return 0.0 } } /// Convert HealthKit valence to Feels Mood func valenceToMood(_ valence: Double) -> Mood { switch valence { case ..<(-0.75): return .horrible case -0.75..<(-0.25): return .bad case -0.25..<0.25: return .average case 0.25..<0.75: return .good default: return .great } } /// Get HealthKit labels for a mood private func labelsForMood(_ mood: Mood) -> [HKStateOfMind.Label] { switch mood { case .horrible: return [.sad, .stressed, .anxious] case .bad: return [.sad, .stressed] case .average: return [.calm, .indifferent] case .good: return [.happy, .calm, .content] case .great: return [.happy, .excited, .joyful] case .missing, .placeholder: return [] } } } // MARK: - Errors enum HealthKitError: LocalizedError { case notAvailable case notAuthorized case typeNotAvailable var errorDescription: String? { switch self { case .notAvailable: return "HealthKit is not available on this device" case .notAuthorized: return "HealthKit access not authorized" case .typeNotAvailable: return "State of Mind type not available" } } }