172 lines
6.4 KiB
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)
|
|
}
|
|
}
|