@@ -2,7 +2,9 @@ import HealthKit
22
33extension HKSample {
44
5- func convertToDictionary( ) -> [ String : Any ] ? {
5+ func convertToDictionary(
6+ healthStore: HKHealthStore
7+ ) async -> [ String : Any ] ? {
68 guard let workout: HKWorkout = self as? HKWorkout else {
79 return nil
810 }
@@ -13,6 +15,11 @@ extension HKSample {
1315 let isoStartDate = self . formatUtcIsoDateTimeString ( workout. startDate)
1416 let isoEndDate = self . formatUtcIsoDateTimeString ( workout. endDate)
1517
18+ let analysisHeartRate = try ? await analyzeWorkoutHeartRate (
19+ workout: workout,
20+ healthStore: healthStore
21+ )
22+
1623 var dataRecords = [
1724 " uuid " : workout. uuid. uuidString as Any ,
1825 " duration " : workout. duration as Any ,
@@ -21,6 +28,9 @@ extension HKSample {
2128 " distance " : distance as Any ,
2229 " type " : workout. workoutActivityType. rawValue as Any ,
2330 " metadata " : workout. metadata as Any ,
31+ " heartRateAverage " : analysisHeartRate? . averageBPM as Any ,
32+ " heartRateMin " : analysisHeartRate? . minBPM as Any ,
33+ " heartRateMax " : analysisHeartRate? . maxBPM as Any ,
2434 " source " : [
2535 " name " : workout. sourceRevision. source. name as Any ,
2636 " device " : sourceDevice as Any ,
@@ -40,3 +50,145 @@ extension HKSample {
4050 }
4151}
4252
53+ @available ( iOS 15 . 0 , * )
54+ func analyzeWorkoutHeartRate(
55+ workout: HKWorkout ,
56+ healthStore: HKHealthStore
57+ ) async throws -> WorkoutHeartRateAnalysis {
58+ async let average = workout. averageHeartRate ( healthStore: healthStore)
59+ async let max = workout. maxHeartRate ( healthStore: healthStore)
60+ async let min = workout. minHeartRate ( healthStore: healthStore)
61+ async let samples = workout. heartRateSamples ( healthStore: healthStore)
62+
63+ return try await WorkoutHeartRateAnalysis (
64+ averageBPM: average,
65+ maxBPM: max,
66+ minBPM: min,
67+ samples: samples
68+ )
69+ }
70+
71+ // MARK: - Data Model
72+
73+ struct WorkoutHeartRateAnalysis {
74+ let averageBPM : Double ?
75+ let maxBPM : Double ?
76+ let minBPM : Double ?
77+ let samples : [ HeartRateSample ]
78+
79+ struct HeartRateSample {
80+ let value : Double
81+ let timestamp : Date
82+ }
83+ }
84+
85+ @available ( iOS 15 . 0 , * )
86+ extension HKWorkout {
87+
88+ func averageHeartRate(
89+ healthStore: HKHealthStore
90+ ) async throws -> Double ? {
91+ try await getHeartRateStatistic (
92+ healthStore: healthStore,
93+ options: . discreteAverage
94+ )
95+ }
96+
97+ func maxHeartRate(
98+ healthStore: HKHealthStore
99+ ) async throws -> Double ? {
100+ try await getHeartRateStatistic (
101+ healthStore: healthStore,
102+ options: . discreteMax
103+ )
104+ }
105+
106+ func minHeartRate(
107+ healthStore: HKHealthStore
108+ ) async throws -> Double ? {
109+ try await getHeartRateStatistic (
110+ healthStore: healthStore,
111+ options: . discreteMin
112+ )
113+ }
114+
115+ func heartRateSamples(
116+ healthStore: HKHealthStore
117+ ) async throws -> [ WorkoutHeartRateAnalysis . HeartRateSample ] {
118+ guard let heartRateType = HKQuantityType . quantityType ( forIdentifier: . heartRate) else {
119+ throw HKError ( . errorInvalidArgument)
120+ }
121+
122+ let predicate = HKQuery . predicateForSamples (
123+ withStart: startDate,
124+ end: endDate,
125+ options: . strictStartDate
126+ )
127+
128+ let samples : [ HKQuantitySample ] = try await withCheckedThrowingContinuation { continuation in
129+ let query = HKSampleQuery (
130+ sampleType: heartRateType,
131+ predicate: predicate,
132+ limit: HKObjectQueryNoLimit,
133+ sortDescriptors: [ . init( key: HKSampleSortIdentifierStartDate, ascending: true ) ]
134+ ) { _, samples, error in
135+ if let error = error {
136+ continuation. resume ( throwing: error)
137+ } else {
138+ continuation. resume ( returning: ( samples as? [ HKQuantitySample ] ) ?? [ ] )
139+ }
140+ }
141+ healthStore. execute ( query)
142+ }
143+
144+ return samples. map {
145+ WorkoutHeartRateAnalysis . HeartRateSample (
146+ value: $0. quantity. doubleValue ( for: . heartRateUnit) ,
147+ timestamp: $0. startDate
148+ )
149+ }
150+ }
151+
152+ private func getHeartRateStatistic(
153+ healthStore: HKHealthStore ,
154+ options: HKStatisticsOptions
155+ ) async throws -> Double ? {
156+ guard let heartRateType = HKQuantityType . quantityType ( forIdentifier: . heartRate) else {
157+ throw HKError ( . errorInvalidArgument)
158+ }
159+
160+ let stats : HKStatistics = try await withCheckedThrowingContinuation { continuation in
161+ let query = HKStatisticsQuery (
162+ quantityType: heartRateType,
163+ quantitySamplePredicate: HKQuery . predicateForSamples (
164+ withStart: startDate,
165+ end: endDate,
166+ options: . strictStartDate
167+ ) ,
168+ options: options
169+ ) { _, statistics, error in
170+ if let error = error {
171+ continuation. resume ( throwing: error)
172+ } else if let stats = statistics {
173+ continuation. resume ( returning: stats)
174+ } else {
175+ continuation. resume ( throwing: HKError ( . errorNoData) )
176+ }
177+ }
178+ healthStore. execute ( query)
179+ }
180+
181+ switch options {
182+ case . discreteAverage: return stats. averageQuantity ( ) ? . doubleValue ( for: . heartRateUnit)
183+ case . discreteMax: return stats. maximumQuantity ( ) ? . doubleValue ( for: . heartRateUnit)
184+ case . discreteMin: return stats. minimumQuantity ( ) ? . doubleValue ( for: . heartRateUnit)
185+ default : return nil
186+ }
187+ }
188+ }
189+
190+ extension HKUnit {
191+ static var heartRateUnit : HKUnit {
192+ HKUnit ( from: " count/min " )
193+ }
194+ }
0 commit comments