Files
WerkoutIOS/iphone/Werkout_ios/HealthKitHelper.swift

172 lines
6.4 KiB
Swift

//
// 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)
}
}