import Foundation import HealthKit class HealthKitManager { private let store = HKHealthStore() var onDataReceived: ((HKQuantityTypeIdentifier, HKQuantitySample?, HKStatistics?) -> Void)? func requestAuthorization() { guard HKHealthStore.isHealthDataAvailable() else { return } let dataTypesAuth: Set = [ HKObjectType.quantityType(forIdentifier: .heartRate)!, HKObjectType.quantityType(forIdentifier: .oxygenSaturation)!, HKObjectType.quantityType(forIdentifier: .numberOfTimesFallen)!, HKObjectType.quantityType(forIdentifier: .respiratoryRate)!, HKObjectType.quantityType(forIdentifier: .appleSleepingWristTemperature)!, HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!, HKObjectType.quantityType(forIdentifier: .stepCount)! ] store.requestAuthorization(toShare: [], read: dataTypesAuth) { success, error in if let error = error { print("HealthKit authorization error: \(error.localizedDescription)") } else { print("HealthKit authorization successful") self.startObservers() } } } /// Call this once at startup func startObservers() { let configs: [(HKQuantityTypeIdentifier, HKUpdateFrequency)] = [ (.numberOfTimesFallen, .immediate), (.heartRate, .hourly), (.oxygenSaturation, .hourly), (.respiratoryRate, .hourly), (.appleSleepingWristTemperature, .hourly), (.activeEnergyBurned, .hourly), (.stepCount, .hourly) ] configs.forEach { id, freq in guard let type = HKObjectType.quantityType(forIdentifier: id) else { return } startObserver(for: type, identifier: id, frequency: freq) } } private func startObserver( for type: HKQuantityType, identifier id: HKQuantityTypeIdentifier, frequency: HKUpdateFrequency ) { let key = "HKAnchor_\(id.rawValue)" // ObserverQuery let observerQuery = HKObserverQuery(sampleType: type, predicate: nil) { [weak self] _, completion, error in guard let self = self else { completion(); return } if let err = error { print("ObserverQuery \(id.rawValue) error:", err) completion() return } print("Observer query \(id.rawValue) fired") // for steps and calories if (id == .stepCount) || (id == .activeEnergyBurned) { self.executeStatisticsQuery(for: type, identifier: id) { sample in if let s = sample { self.onDataReceived?(id, nil, s) } completion() } } else { let hasAnchor = UserDefaults.standard.data(forKey: key) != nil if hasAnchor { // subsequent runs → anchored query limit=1 self.fetchAnchoredSample(after: self.loadAnchor(for: key), for: type, identifier: id, limit: 1) { sample in if let s = sample { self.onDataReceived?(id, s, nil) } completion() } } else { // first run → sampleQuery(limit=1), then prime anchor self.fetchLatestSampleOnce(for: type) { sample in if let s = sample { self.onDataReceived?(id, s, nil) } self.primeAnchor(for: type, key: key) { completion() } } } } } store.execute(observerQuery) store.enableBackgroundDelivery(for: type, frequency: frequency) { success, error in if let error = error { print("BackgroundDelivery \(id.rawValue) error:", error) } else { print("BackgroundDelivery \(id.rawValue) enabled") } } } private func executeStatisticsQuery( for type: HKQuantityType, identifier id: HKQuantityTypeIdentifier, completion: @escaping (HKStatistics?) -> Void ) { let calendar = Calendar.current let now = Date() let startOfDay = calendar.startOfDay(for: now) let predicateSinceMidnight = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate) let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicateSinceMidnight, options: .cumulativeSum) { _, result, error in if let err = error { print("StatisticsQuery \(type.identifier) error:", err) completion(nil) } else { print("StatiscticsQuery \(type.identifier) executed") completion(result) } } store.execute(query) } /// HKSampleQuery(limit=1) private func fetchLatestSampleOnce( for type: HKQuantityType, completion: @escaping (HKQuantitySample?) -> Void ) { let sort = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery(sampleType: type, predicate: nil, limit: 1, sortDescriptors: [sort]) { _, results, error in if let err = error { print("SampleQuery \(type.identifier) error:", err) completion(nil) } else { print("sample query \(type.identifier) executed") completion(results?.first as? HKQuantitySample) } } store.execute(query) } /// HKAnchoredObjectQuery(limit=0) just to prime private func primeAnchor( for type: HKQuantityType, key: String, completion: @escaping () -> Void ) { let query = HKAnchoredObjectQuery(type: type, predicate: nil, anchor: nil, limit: 0) { [weak self] _, _, _, newAnchor, error in guard let self = self else { completion(); return } if let err = error { print("PrimeAnchor \(type.identifier) error:", err) } else if let a = newAnchor { print("prime anchor \(type.identifier) saved") self.saveAnchor(a, for: key) } else { print("no new anchor found") } completion() } store.execute(query) } /// HKAnchoredObjectQuery(limit=N) private func fetchAnchoredSample( after anchor: HKQueryAnchor?, for type: HKQuantityType, identifier id: HKQuantityTypeIdentifier, limit: Int, completion: @escaping (HKQuantitySample?) -> Void ) { let query = HKAnchoredObjectQuery(type: type, predicate: nil, anchor: anchor, limit: HKObjectQueryNoLimit) { [weak self] _, samples, _, newAnchor, error in guard let self = self else { completion(nil); return } if let err = error { print("AnchoredQuery \(type.identifier) error:", err) completion(nil); return } print("Anchor query \(type.identifier) executed") guard let samples = samples as? [HKQuantitySample], !samples.isEmpty else { print("no samples found") completion(nil); return } if let latestSample = samples.max(by: { $0.endDate < $1.endDate }) { completion(latestSample) } else { print("no latest sample found") completion(nil); return } if let anchor = newAnchor { self.saveAnchor(anchor, for: "HKAnchor_\(id.rawValue)") } } store.execute(query) } // MARK: - Anchor Persistence private func loadAnchor(for key: String) -> HKQueryAnchor? { guard let d = UserDefaults.standard.data(forKey: key) else { return nil } print("loaded anchor \(key)") return try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: d) } private func saveAnchor(_ anchor: HKQueryAnchor, for key: String) { if let d = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true) { UserDefaults.standard.set(d, forKey: key) } print("anchor saved \(key)") } }