// // HealthKitHelper.swift // Werkout_ios // // Created by Trey Tartt on 8/13/23. // import Foundation import HealthKit import SharedCore struct HealthKitWorkoutData { var caloriesBurned: Double? var minHeartRate: Double? var maxHeartRate: Double? var avgHeartRate: Double? } class HealthKitHelper { private let runtimeReporter = RuntimeReporter.shared let healthStore = HKHealthStore() func getDetails(forHealthKitUUID uuid: UUID, completion: @escaping ((HealthKitWorkoutData?) -> Void)) { runtimeReporter.recordInfo("Fetching HealthKit workout details", metadata: ["uuid": uuid.uuidString]) let query = HKSampleQuery(sampleType: HKWorkoutType.workoutType(), predicate: HKQuery.predicateForObject(with: uuid), limit: 1, sortDescriptors: nil) { [weak self] (_, results, error) -> Void in guard let self else { DispatchQueue.main.async { completion(nil) } return } if let queryError = error { self.runtimeReporter.recordError( "Failed querying HealthKit workout", metadata: ["error": queryError.localizedDescription] ) DispatchQueue.main.async { completion(nil) } return } guard let workout = results?.compactMap({ $0 as? HKWorkout }).first else { self.runtimeReporter.recordWarning("No HealthKit workout found for UUID", metadata: ["uuid": uuid.uuidString]) DispatchQueue.main.async { completion(nil) } return } self.collectDetails(forWorkout: workout, completion: completion) } healthStore.execute(query) } private func collectDetails(forWorkout workout: HKWorkout, completion: @escaping ((HealthKitWorkoutData?) -> Void)) { let aggregateQueue = DispatchQueue(label: "com.werkout.healthkit.aggregate") var workoutData = HealthKitWorkoutData( caloriesBurned: nil, minHeartRate: nil, maxHeartRate: nil, avgHeartRate: nil ) let group = DispatchGroup() group.enter() getTotalBurned(forWorkout: workout) { calories in aggregateQueue.async { workoutData.caloriesBurned = calories group.leave() } } group.enter() getHeartRateStuff(forWorkout: workout) { heartRateData in aggregateQueue.async { workoutData.minHeartRate = heartRateData?.minHeartRate workoutData.maxHeartRate = heartRateData?.maxHeartRate workoutData.avgHeartRate = heartRateData?.avgHeartRate group.leave() } } group.notify(queue: .main) { let hasValues = workoutData.caloriesBurned != nil || workoutData.minHeartRate != nil || workoutData.maxHeartRate != nil || workoutData.avgHeartRate != nil completion(hasValues ? workoutData : nil) } } private func getHeartRateStuff(forWorkout workout: HKWorkout, completion: @escaping ((HealthKitWorkoutData?) -> Void)) { guard let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate) else { completion(nil) return } let heartPredicate = HKQuery.predicateForSamples(withStart: workout.startDate, end: workout.endDate, options: HKQueryOptions.strictEndDate) let heartQuery = HKStatisticsQuery(quantityType: heartType, quantitySamplePredicate: heartPredicate, options: [.discreteAverage, .discreteMin, .discreteMax], completionHandler: { [weak self] (_, result, error) -> Void in if let error { self?.runtimeReporter.recordError( "Failed querying HealthKit heart rate stats", metadata: ["error": error.localizedDescription] ) completion(nil) return } guard let result, let minValue = result.minimumQuantity(), let maxValue = result.maximumQuantity(), let avgValue = result.averageQuantity() else { completion(nil) return } let unit = HKUnit(from: "count/min") let data = HealthKitWorkoutData( caloriesBurned: nil, minHeartRate: minValue.doubleValue(for: unit), maxHeartRate: maxValue.doubleValue(for: unit), avgHeartRate: avgValue.doubleValue(for: unit) ) completion(data) }) healthStore.execute(heartQuery) } private func getTotalBurned(forWorkout workout: HKWorkout, completion: @escaping ((Double?) -> Void)) { guard let calType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) else { completion(nil) return } let calPredicate = HKQuery.predicateForSamples(withStart: workout.startDate, end: workout.endDate, options: HKQueryOptions.strictEndDate) let calQuery = HKStatisticsQuery(quantityType: calType, quantitySamplePredicate: calPredicate, options: [.cumulativeSum], completionHandler: { [weak self] (_, result, error) -> Void in if let error { self?.runtimeReporter.recordError( "Failed querying HealthKit calories", metadata: ["error": error.localizedDescription] ) completion(nil) return } completion(result?.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie())) }) healthStore.execute(calQuery) } }