@appeeky/expo-healthkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +92 -0
- package/README.md +453 -0
- package/android/build.gradle +22 -0
- package/android/src/main/AndroidManifest.xml +74 -0
- package/android/src/main/java/expo/modules/healthkit/ExpoHealthKitModule.kt +228 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectMapping.kt +129 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectNutrition.kt +152 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectRationaleActivity.kt +62 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectService.kt +1054 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectUnits.kt +133 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectWorkouts.kt +105 -0
- package/android/src/main/java/expo/modules/healthkit/HealthKitExceptions.kt +30 -0
- package/app.plugin.js +1 -0
- package/build/ExpoHealthKitModule.d.ts +39 -0
- package/build/ExpoHealthKitModule.d.ts.map +1 -0
- package/build/ExpoHealthKitModule.js +35 -0
- package/build/ExpoHealthKitModule.js.map +1 -0
- package/build/ExpoHealthKitModule.web.d.ts +39 -0
- package/build/ExpoHealthKitModule.web.d.ts.map +1 -0
- package/build/ExpoHealthKitModule.web.js +105 -0
- package/build/ExpoHealthKitModule.web.js.map +1 -0
- package/build/dates.d.ts +6 -0
- package/build/dates.d.ts.map +1 -0
- package/build/dates.js +13 -0
- package/build/dates.js.map +1 -0
- package/build/errors.d.ts +9 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +18 -0
- package/build/errors.js.map +1 -0
- package/build/identifiers.d.ts +418 -0
- package/build/identifiers.d.ts.map +1 -0
- package/build/identifiers.js +402 -0
- package/build/identifiers.js.map +1 -0
- package/build/index.d.ts +454 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +418 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +542 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/docs/banner.jpg +0 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoHealthKit.podspec +30 -0
- package/ios/ExpoHealthKitModule.swift +155 -0
- package/ios/HealthKitAdvancedQueries.swift +458 -0
- package/ios/HealthKitExceptions.swift +109 -0
- package/ios/HealthKitIdentifiers.swift +183 -0
- package/ios/HealthKitRecords.swift +214 -0
- package/ios/HealthKitService.swift +626 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/package.json +105 -0
- package/plugin/build/index.d.ts +30 -0
- package/plugin/build/index.js +138 -0
- package/src/ExpoHealthKitModule.ts +116 -0
- package/src/ExpoHealthKitModule.web.ts +140 -0
- package/src/dates.ts +17 -0
- package/src/errors.ts +22 -0
- package/src/identifiers.ts +494 -0
- package/src/index.ts +563 -0
- package/src/types.ts +625 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import HealthKit
|
|
2
|
+
|
|
3
|
+
internal final class HealthKitService {
|
|
4
|
+
let store = HKHealthStore()
|
|
5
|
+
var onUpdate: ((String) -> Void)?
|
|
6
|
+
|
|
7
|
+
private var observerQueries: [String: HKObserverQuery] = [:]
|
|
8
|
+
|
|
9
|
+
func executeQuery(_ query: HKQuery) throws {
|
|
10
|
+
try catchingHealthKit {
|
|
11
|
+
self.store.execute(query)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func executeQuery<T: Sendable>(_ query: HKQuery, continuation: CheckedContinuation<T, Error>) {
|
|
16
|
+
do {
|
|
17
|
+
try executeQuery(query)
|
|
18
|
+
} catch {
|
|
19
|
+
continuation.resume(throwing: error)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func isAvailable() -> Bool {
|
|
24
|
+
HKHealthStore.isHealthDataAvailable()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func requestAuthorization(_ options: AuthorizationOptions) async throws -> Bool {
|
|
28
|
+
try ensureAvailable()
|
|
29
|
+
|
|
30
|
+
if options.toRead.isEmpty && options.toShare.isEmpty {
|
|
31
|
+
throw EmptyPermissionsException()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let readTypes = Set(try options.toRead.map { try HealthKitIdentifiers.objectType(for: $0) })
|
|
35
|
+
let shareTypes = Set(try options.toShare.map { try HealthKitIdentifiers.sampleType(for: $0) })
|
|
36
|
+
|
|
37
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
38
|
+
store.requestAuthorization(toShare: shareTypes, read: readTypes) { success, error in
|
|
39
|
+
if let error {
|
|
40
|
+
continuation.resume(throwing: error)
|
|
41
|
+
} else {
|
|
42
|
+
continuation.resume(returning: success)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func authorizationStatus(for identifier: String) throws -> Int {
|
|
49
|
+
try ensureAvailable()
|
|
50
|
+
let type = try HealthKitIdentifiers.objectType(for: identifier)
|
|
51
|
+
return Int(store.authorizationStatus(for: type).rawValue)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func requestStatusForAuthorization(_ options: AuthorizationOptions) async throws -> Int {
|
|
55
|
+
try ensureAvailable()
|
|
56
|
+
let readTypes = Set(try options.toRead.map { try HealthKitIdentifiers.objectType(for: $0) })
|
|
57
|
+
let shareTypes = Set(try options.toShare.map { try HealthKitIdentifiers.sampleType(for: $0) })
|
|
58
|
+
|
|
59
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
60
|
+
store.getRequestStatusForAuthorization(toShare: shareTypes, read: readTypes) { status, error in
|
|
61
|
+
if let error {
|
|
62
|
+
continuation.resume(throwing: error)
|
|
63
|
+
} else {
|
|
64
|
+
continuation.resume(returning: Int(status.rawValue))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func queryQuantitySamples(_ options: QuantityQueryOptions) async throws -> [[String: Any]] {
|
|
71
|
+
try ensureAvailable()
|
|
72
|
+
let quantityType = try HealthKitIdentifiers.quantityType(for: options.type)
|
|
73
|
+
let unit = try HealthKitIdentifiers.unit(from: options.unit)
|
|
74
|
+
let samples = try await sampleQuery(
|
|
75
|
+
sampleType: quantityType,
|
|
76
|
+
from: options.from,
|
|
77
|
+
to: options.to,
|
|
78
|
+
limit: options.limit,
|
|
79
|
+
ascending: options.ascending
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return samples.compactMap { sample in
|
|
83
|
+
guard let quantitySample = sample as? HKQuantitySample else {
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
return mapQuantitySample(quantitySample, type: options.type, unit: unit)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func queryCategorySamples(_ options: CategoryQueryOptions) async throws -> [[String: Any]] {
|
|
91
|
+
try ensureAvailable()
|
|
92
|
+
let categoryType = try HealthKitIdentifiers.categoryType(for: options.type)
|
|
93
|
+
let samples = try await sampleQuery(
|
|
94
|
+
sampleType: categoryType,
|
|
95
|
+
from: options.from,
|
|
96
|
+
to: options.to,
|
|
97
|
+
limit: options.limit,
|
|
98
|
+
ascending: options.ascending
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return samples.compactMap { sample in
|
|
102
|
+
guard let categorySample = sample as? HKCategorySample else {
|
|
103
|
+
return nil
|
|
104
|
+
}
|
|
105
|
+
return mapCategorySample(categorySample, type: options.type)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func queryWorkouts(_ options: WorkoutQueryOptions) async throws -> [[String: Any]] {
|
|
110
|
+
try ensureAvailable()
|
|
111
|
+
var predicate = try samplePredicate(from: options.from, to: options.to)
|
|
112
|
+
if let activityTypeRaw = options.activityType,
|
|
113
|
+
let activityType = HKWorkoutActivityType(rawValue: UInt(activityTypeRaw))
|
|
114
|
+
{
|
|
115
|
+
let activityPredicate = HKQuery.predicateForWorkouts(with: activityType)
|
|
116
|
+
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, activityPredicate].compactMap { $0 })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let samples = try await executeSampleQuery(
|
|
120
|
+
sampleType: HKObjectType.workoutType(),
|
|
121
|
+
predicate: predicate,
|
|
122
|
+
limit: HealthKitIdentifiers.queryLimit(options.limit),
|
|
123
|
+
ascending: options.ascending
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return samples.compactMap { sample in
|
|
127
|
+
guard let workout = sample as? HKWorkout else {
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
return mapWorkout(workout)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func queryStatistics(_ options: StatisticsQueryOptions) async throws -> [String: Any] {
|
|
135
|
+
try ensureAvailable()
|
|
136
|
+
let quantityType = try HealthKitIdentifiers.quantityType(for: options.type)
|
|
137
|
+
let unit = try HealthKitIdentifiers.unit(from: options.unit)
|
|
138
|
+
let predicate = try samplePredicate(from: options.from, to: options.to)
|
|
139
|
+
let statisticsOptions = HealthKitIdentifiers.statisticsOptions(from: options.options, quantityType: quantityType)
|
|
140
|
+
|
|
141
|
+
let statistics: HKStatistics? = try await withCheckedThrowingContinuation { continuation in
|
|
142
|
+
let query = HKStatisticsQuery(
|
|
143
|
+
quantityType: quantityType,
|
|
144
|
+
quantitySamplePredicate: predicate,
|
|
145
|
+
options: statisticsOptions
|
|
146
|
+
) { _, stats, error in
|
|
147
|
+
if let error {
|
|
148
|
+
continuation.resume(throwing: error)
|
|
149
|
+
} else {
|
|
150
|
+
continuation.resume(returning: stats)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
executeQuery(query, continuation: continuation)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
guard let statistics else {
|
|
157
|
+
let now = HealthKitIdentifiers.isoString(from: Date())
|
|
158
|
+
return [
|
|
159
|
+
"startDate": now,
|
|
160
|
+
"endDate": now,
|
|
161
|
+
"unit": unit.unitString
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return mapStatistics(statistics, unit: unit)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
func queryStatisticsCollection(_ options: StatisticsCollectionQueryOptions) async throws -> [[String: Any]] {
|
|
169
|
+
try ensureAvailable()
|
|
170
|
+
let quantityType = try HealthKitIdentifiers.quantityType(for: options.type)
|
|
171
|
+
let unit = try HealthKitIdentifiers.unit(from: options.unit)
|
|
172
|
+
let from = try HealthKitIdentifiers.optionalDate(from: options.from)
|
|
173
|
+
?? Calendar.current.date(byAdding: .day, value: -7, to: Date())
|
|
174
|
+
?? Date()
|
|
175
|
+
let to = try HealthKitIdentifiers.optionalDate(from: options.to) ?? Date()
|
|
176
|
+
let statisticsOptions = HealthKitIdentifiers.statisticsOptions(from: options.options, quantityType: quantityType)
|
|
177
|
+
let interval = dateComponents(from: options)
|
|
178
|
+
let anchorDate = Calendar.current.startOfDay(for: from == Date.distantPast ? to : from)
|
|
179
|
+
|
|
180
|
+
let collection: HKStatisticsCollection? = try await withCheckedThrowingContinuation { continuation in
|
|
181
|
+
let query = HKStatisticsCollectionQuery(
|
|
182
|
+
quantityType: quantityType,
|
|
183
|
+
quantitySamplePredicate: nil,
|
|
184
|
+
options: statisticsOptions,
|
|
185
|
+
anchorDate: anchorDate,
|
|
186
|
+
intervalComponents: interval
|
|
187
|
+
)
|
|
188
|
+
query.initialResultsHandler = { _, results, error in
|
|
189
|
+
if let error {
|
|
190
|
+
continuation.resume(throwing: error)
|
|
191
|
+
} else {
|
|
192
|
+
continuation.resume(returning: results)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
executeQuery(query, continuation: continuation)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
var records: [[String: Any]] = []
|
|
199
|
+
collection?.enumerateStatistics(from: from, to: to) { stats, _ in
|
|
200
|
+
records.append(self.mapStatistics(stats, unit: unit))
|
|
201
|
+
}
|
|
202
|
+
return records
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func queryAnchored(_ options: AnchoredQueryOptions) async throws -> [String: Any] {
|
|
206
|
+
try ensureAvailable()
|
|
207
|
+
let sampleType = try HealthKitIdentifiers.sampleType(for: options.type)
|
|
208
|
+
let predicate = try samplePredicate(from: options.from, to: options.to)
|
|
209
|
+
let anchor = try HealthKitIdentifiers.decodeAnchor(options.anchor)
|
|
210
|
+
let unit = try options.unit.map { try HealthKitIdentifiers.unit(from: $0) }
|
|
211
|
+
|
|
212
|
+
let result: (added: [HKSample], deleted: [HKDeletedObject], anchor: HKQueryAnchor?) = try await withCheckedThrowingContinuation { continuation in
|
|
213
|
+
let query = HKAnchoredObjectQuery(
|
|
214
|
+
type: sampleType,
|
|
215
|
+
predicate: predicate,
|
|
216
|
+
anchor: anchor,
|
|
217
|
+
limit: HealthKitIdentifiers.queryLimit(options.limit)
|
|
218
|
+
) { _, added, deleted, newAnchor, error in
|
|
219
|
+
if let error {
|
|
220
|
+
continuation.resume(throwing: error)
|
|
221
|
+
} else {
|
|
222
|
+
continuation.resume(returning: (added ?? [], deleted ?? [], newAnchor))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
executeQuery(query, continuation: continuation)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let added = result.added.compactMap { sample -> [String: Any]? in
|
|
229
|
+
guard let quantitySample = sample as? HKQuantitySample, let unit else {
|
|
230
|
+
return nil
|
|
231
|
+
}
|
|
232
|
+
return mapQuantitySample(quantitySample, type: options.type, unit: unit)
|
|
233
|
+
}
|
|
234
|
+
let deleted = result.deleted.map { item in
|
|
235
|
+
["uuid": item.uuid.uuidString]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
var payload: [String: Any] = [
|
|
239
|
+
"added": added,
|
|
240
|
+
"deleted": deleted
|
|
241
|
+
]
|
|
242
|
+
if let encoded = try HealthKitIdentifiers.encodeAnchor(result.anchor) {
|
|
243
|
+
payload["anchor"] = encoded
|
|
244
|
+
}
|
|
245
|
+
return payload
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func saveQuantitySample(_ input: QuantitySampleInput) async throws -> String {
|
|
249
|
+
try ensureAvailable()
|
|
250
|
+
let sample = try makeQuantitySample(input)
|
|
251
|
+
try await store.save(sample)
|
|
252
|
+
return sample.uuid.uuidString
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
func makeQuantitySample(_ input: QuantitySampleInput) throws -> HKQuantitySample {
|
|
256
|
+
let quantityType = try HealthKitIdentifiers.quantityType(for: input.type)
|
|
257
|
+
let unit = try HealthKitIdentifiers.unit(from: input.unit)
|
|
258
|
+
let start = try HealthKitIdentifiers.date(from: input.startDate)
|
|
259
|
+
let end = try HealthKitIdentifiers.date(from: input.endDate)
|
|
260
|
+
return HKQuantitySample(
|
|
261
|
+
type: quantityType,
|
|
262
|
+
quantity: HKQuantity(unit: unit, doubleValue: input.value),
|
|
263
|
+
start: start,
|
|
264
|
+
end: end,
|
|
265
|
+
metadata: input.metadata
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func saveCategorySample(_ input: CategorySampleInput) async throws -> String {
|
|
270
|
+
try ensureAvailable()
|
|
271
|
+
let categoryType = try HealthKitIdentifiers.categoryType(for: input.type)
|
|
272
|
+
let start = try HealthKitIdentifiers.date(from: input.startDate)
|
|
273
|
+
let end = try HealthKitIdentifiers.date(from: input.endDate)
|
|
274
|
+
let sample = HKCategorySample(
|
|
275
|
+
type: categoryType,
|
|
276
|
+
value: input.value,
|
|
277
|
+
start: start,
|
|
278
|
+
end: end,
|
|
279
|
+
metadata: input.metadata
|
|
280
|
+
)
|
|
281
|
+
try await store.save(sample)
|
|
282
|
+
return sample.uuid.uuidString
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func saveWorkout(_ input: WorkoutInput) async throws -> String {
|
|
286
|
+
try ensureAvailable()
|
|
287
|
+
guard let activityType = HKWorkoutActivityType(rawValue: UInt(input.activityType)) else {
|
|
288
|
+
throw InvalidIdentifierException("workoutActivityType \(input.activityType)")
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let start = try HealthKitIdentifiers.date(from: input.startDate)
|
|
292
|
+
let end = try HealthKitIdentifiers.date(from: input.endDate)
|
|
293
|
+
let energy = try quantity(value: input.energyBurned, unitString: input.energyBurnedUnit ?? "kcal")
|
|
294
|
+
let distance = try quantity(value: input.distance, unitString: input.distanceUnit ?? "m")
|
|
295
|
+
|
|
296
|
+
let workout = HKWorkout(
|
|
297
|
+
activityType: activityType,
|
|
298
|
+
start: start,
|
|
299
|
+
end: end,
|
|
300
|
+
duration: end.timeIntervalSince(start),
|
|
301
|
+
totalEnergyBurned: energy,
|
|
302
|
+
totalDistance: distance,
|
|
303
|
+
metadata: input.metadata
|
|
304
|
+
)
|
|
305
|
+
try await store.save(workout)
|
|
306
|
+
return workout.uuid.uuidString
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
func deleteObjects(_ options: DeleteObjectsOptions) async throws -> Int {
|
|
310
|
+
try ensureAvailable()
|
|
311
|
+
guard let typeIdentifier = options.type else {
|
|
312
|
+
throw MissingDeleteTypeException()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let sampleType = try HealthKitIdentifiers.sampleType(for: typeIdentifier)
|
|
316
|
+
var predicates: [NSPredicate] = []
|
|
317
|
+
|
|
318
|
+
if let uuidString = options.uuid {
|
|
319
|
+
guard let uuid = UUID(uuidString: uuidString) else {
|
|
320
|
+
throw MissingUuidException()
|
|
321
|
+
}
|
|
322
|
+
predicates.append(HKQuery.predicateForObject(with: uuid))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if let datePredicate = try samplePredicate(from: options.from, to: options.to) {
|
|
326
|
+
predicates.append(datePredicate)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let predicate: NSPredicate? = {
|
|
330
|
+
if predicates.isEmpty { return nil }
|
|
331
|
+
if predicates.count == 1 { return predicates[0] }
|
|
332
|
+
return NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
|
333
|
+
}()
|
|
334
|
+
|
|
335
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
336
|
+
store.deleteObjects(of: sampleType, predicate: predicate ?? HKQuery.predicateForSamples(withStart: .distantPast, end: Date(), options: [])) { success, deletedCount, error in
|
|
337
|
+
if let error {
|
|
338
|
+
continuation.resume(throwing: error)
|
|
339
|
+
} else if success {
|
|
340
|
+
continuation.resume(returning: Int(deletedCount))
|
|
341
|
+
} else {
|
|
342
|
+
continuation.resume(returning: 0)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
func biologicalSex() throws -> Int {
|
|
349
|
+
try ensureAvailable()
|
|
350
|
+
return try readCharacteristic {
|
|
351
|
+
Int(try store.biologicalSex().biologicalSex.rawValue)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
func bloodType() throws -> Int {
|
|
356
|
+
try ensureAvailable()
|
|
357
|
+
return try readCharacteristic {
|
|
358
|
+
Int(try store.bloodType().bloodType.rawValue)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func dateOfBirth() throws -> String? {
|
|
363
|
+
try ensureAvailable()
|
|
364
|
+
do {
|
|
365
|
+
let components = try store.dateOfBirthComponents()
|
|
366
|
+
guard let date = Calendar.current.date(from: components) else {
|
|
367
|
+
return nil
|
|
368
|
+
}
|
|
369
|
+
return HealthKitIdentifiers.isoString(from: date)
|
|
370
|
+
} catch {
|
|
371
|
+
if isNoData(error) {
|
|
372
|
+
return nil
|
|
373
|
+
}
|
|
374
|
+
throw error
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
func fitzpatrickSkinType() throws -> Int {
|
|
379
|
+
try ensureAvailable()
|
|
380
|
+
return try readCharacteristic {
|
|
381
|
+
Int(try store.fitzpatrickSkinType().skinType.rawValue)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
func wheelchairUse() throws -> Int {
|
|
386
|
+
try ensureAvailable()
|
|
387
|
+
return try readCharacteristic {
|
|
388
|
+
Int(try store.wheelchairUse().wheelchairUse.rawValue)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func enableBackgroundDelivery(type identifier: String, frequency: Int) async throws -> Bool {
|
|
393
|
+
try ensureAvailable()
|
|
394
|
+
let type = try HealthKitIdentifiers.objectType(for: identifier)
|
|
395
|
+
let updateFrequency = HKUpdateFrequency(rawValue: Int(frequency)) ?? .hourly
|
|
396
|
+
|
|
397
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
398
|
+
store.enableBackgroundDelivery(for: type, frequency: updateFrequency) { success, error in
|
|
399
|
+
if let error {
|
|
400
|
+
continuation.resume(throwing: error)
|
|
401
|
+
} else {
|
|
402
|
+
continuation.resume(returning: success)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func disableBackgroundDelivery(type identifier: String) async throws -> Bool {
|
|
409
|
+
try ensureAvailable()
|
|
410
|
+
let type = try HealthKitIdentifiers.objectType(for: identifier)
|
|
411
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
412
|
+
store.disableBackgroundDelivery(for: type) { success, error in
|
|
413
|
+
if let error {
|
|
414
|
+
continuation.resume(throwing: error)
|
|
415
|
+
} else {
|
|
416
|
+
continuation.resume(returning: success)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
func disableAllBackgroundDelivery() async throws -> Bool {
|
|
423
|
+
try ensureAvailable()
|
|
424
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
425
|
+
store.disableAllBackgroundDelivery { success, error in
|
|
426
|
+
if let error {
|
|
427
|
+
continuation.resume(throwing: error)
|
|
428
|
+
} else {
|
|
429
|
+
continuation.resume(returning: success)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
func startObserving(_ identifiers: [String]) throws {
|
|
436
|
+
try ensureAvailable()
|
|
437
|
+
stopObserving()
|
|
438
|
+
|
|
439
|
+
for identifier in identifiers {
|
|
440
|
+
let sampleType = try HealthKitIdentifiers.sampleType(for: identifier)
|
|
441
|
+
let query = HKObserverQuery(sampleType: sampleType, predicate: nil) { [weak self] _, completionHandler, error in
|
|
442
|
+
defer { completionHandler() }
|
|
443
|
+
guard error == nil else {
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
self?.onUpdate?(identifier)
|
|
447
|
+
}
|
|
448
|
+
observerQueries[identifier] = query
|
|
449
|
+
try executeQuery(query)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
func stopObserving() {
|
|
454
|
+
for query in observerQueries.values {
|
|
455
|
+
store.stop(query)
|
|
456
|
+
}
|
|
457
|
+
observerQueries.removeAll()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
func ensureAvailable() throws {
|
|
461
|
+
if !isAvailable() {
|
|
462
|
+
throw HealthUnavailableException()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
func sampleQuery(
|
|
467
|
+
sampleType: HKSampleType,
|
|
468
|
+
from: String?,
|
|
469
|
+
to: String?,
|
|
470
|
+
limit: Int,
|
|
471
|
+
ascending: Bool
|
|
472
|
+
) async throws -> [HKSample] {
|
|
473
|
+
try await executeSampleQuery(
|
|
474
|
+
sampleType: sampleType,
|
|
475
|
+
predicate: try samplePredicate(from: from, to: to),
|
|
476
|
+
limit: HealthKitIdentifiers.queryLimit(limit),
|
|
477
|
+
ascending: ascending
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
func executeSampleQuery(
|
|
482
|
+
sampleType: HKSampleType,
|
|
483
|
+
predicate: NSPredicate?,
|
|
484
|
+
limit: Int,
|
|
485
|
+
ascending: Bool
|
|
486
|
+
) async throws -> [HKSample] {
|
|
487
|
+
let sort = [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: ascending)]
|
|
488
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
489
|
+
let query = HKSampleQuery(
|
|
490
|
+
sampleType: sampleType,
|
|
491
|
+
predicate: predicate,
|
|
492
|
+
limit: limit,
|
|
493
|
+
sortDescriptors: sort
|
|
494
|
+
) { _, samples, error in
|
|
495
|
+
if let error {
|
|
496
|
+
continuation.resume(throwing: error)
|
|
497
|
+
} else {
|
|
498
|
+
continuation.resume(returning: samples ?? [])
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
executeQuery(query, continuation: continuation)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private func samplePredicate(from: String?, to: String?) throws -> NSPredicate? {
|
|
506
|
+
let start = try HealthKitIdentifiers.optionalDate(from: from)
|
|
507
|
+
let end = try HealthKitIdentifiers.optionalDate(from: to)
|
|
508
|
+
if start == nil && end == nil {
|
|
509
|
+
return nil
|
|
510
|
+
}
|
|
511
|
+
return try catchingHealthKit {
|
|
512
|
+
HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private func dateComponents(from options: StatisticsCollectionQueryOptions) -> DateComponents {
|
|
517
|
+
var components = DateComponents()
|
|
518
|
+
if options.year != 0 { components.year = options.year }
|
|
519
|
+
if options.month != 0 { components.month = options.month }
|
|
520
|
+
if options.day != 0 { components.day = options.day }
|
|
521
|
+
if options.hour != 0 { components.hour = options.hour }
|
|
522
|
+
if options.minute != 0 { components.minute = options.minute }
|
|
523
|
+
if options.second != 0 { components.second = options.second }
|
|
524
|
+
if components.year == nil && components.month == nil && components.day == nil
|
|
525
|
+
&& components.hour == nil && components.minute == nil && components.second == nil
|
|
526
|
+
{
|
|
527
|
+
components.day = 1
|
|
528
|
+
}
|
|
529
|
+
return components
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private func quantity(value: Double?, unitString: String) throws -> HKQuantity? {
|
|
533
|
+
guard let value else {
|
|
534
|
+
return nil
|
|
535
|
+
}
|
|
536
|
+
let unit = try HealthKitIdentifiers.unit(from: unitString)
|
|
537
|
+
return HKQuantity(unit: unit, doubleValue: value)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
func mapQuantitySample(_ sample: HKQuantitySample, type: String? = nil, unit: HKUnit? = nil) -> [String: Any] {
|
|
541
|
+
let resolvedUnit = unit ?? HealthKitIdentifiers.displayUnit(for: sample.quantityType)
|
|
542
|
+
let resolvedType = type ?? sample.quantityType.identifier
|
|
543
|
+
var record: [String: Any] = [
|
|
544
|
+
"uuid": sample.uuid.uuidString,
|
|
545
|
+
"type": resolvedType,
|
|
546
|
+
"startDate": HealthKitIdentifiers.isoString(from: sample.startDate),
|
|
547
|
+
"endDate": HealthKitIdentifiers.isoString(from: sample.endDate),
|
|
548
|
+
"value": sample.quantity.doubleValue(for: resolvedUnit),
|
|
549
|
+
"unit": resolvedUnit.unitString
|
|
550
|
+
]
|
|
551
|
+
record["sourceName"] = sample.sourceRevision.source.name
|
|
552
|
+
record["sourceId"] = sample.sourceRevision.source.bundleIdentifier
|
|
553
|
+
record["metadata"] = HealthKitIdentifiers.stringifyMetadata(sample.metadata)
|
|
554
|
+
return record
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private func mapCategorySample(_ sample: HKCategorySample, type: String) -> [String: Any] {
|
|
558
|
+
var record: [String: Any] = [
|
|
559
|
+
"uuid": sample.uuid.uuidString,
|
|
560
|
+
"type": type,
|
|
561
|
+
"startDate": HealthKitIdentifiers.isoString(from: sample.startDate),
|
|
562
|
+
"endDate": HealthKitIdentifiers.isoString(from: sample.endDate),
|
|
563
|
+
"value": sample.value
|
|
564
|
+
]
|
|
565
|
+
record["sourceName"] = sample.sourceRevision.source.name
|
|
566
|
+
record["sourceId"] = sample.sourceRevision.source.bundleIdentifier
|
|
567
|
+
record["metadata"] = HealthKitIdentifiers.stringifyMetadata(sample.metadata)
|
|
568
|
+
return record
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private func mapWorkout(_ workout: HKWorkout) -> [String: Any] {
|
|
572
|
+
var record: [String: Any] = [
|
|
573
|
+
"uuid": workout.uuid.uuidString,
|
|
574
|
+
"type": "HKWorkoutTypeIdentifier",
|
|
575
|
+
"startDate": HealthKitIdentifiers.isoString(from: workout.startDate),
|
|
576
|
+
"endDate": HealthKitIdentifiers.isoString(from: workout.endDate),
|
|
577
|
+
"duration": workout.duration,
|
|
578
|
+
"workoutActivityType": Int(workout.workoutActivityType.rawValue)
|
|
579
|
+
]
|
|
580
|
+
record["sourceName"] = workout.sourceRevision.source.name
|
|
581
|
+
record["sourceId"] = workout.sourceRevision.source.bundleIdentifier
|
|
582
|
+
record["metadata"] = HealthKitIdentifiers.stringifyMetadata(workout.metadata)
|
|
583
|
+
|
|
584
|
+
if let energy = workout.totalEnergyBurned {
|
|
585
|
+
let unit = HKUnit.kilocalorie()
|
|
586
|
+
record["totalEnergyBurned"] = energy.doubleValue(for: unit)
|
|
587
|
+
record["totalEnergyBurnedUnit"] = unit.unitString
|
|
588
|
+
}
|
|
589
|
+
if let distance = workout.totalDistance {
|
|
590
|
+
let unit = HKUnit.meter()
|
|
591
|
+
record["totalDistance"] = distance.doubleValue(for: unit)
|
|
592
|
+
record["totalDistanceUnit"] = unit.unitString
|
|
593
|
+
}
|
|
594
|
+
return record
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private func mapStatistics(_ statistics: HKStatistics, unit: HKUnit) -> [String: Any] {
|
|
598
|
+
var record: [String: Any] = [
|
|
599
|
+
"startDate": HealthKitIdentifiers.isoString(from: statistics.startDate),
|
|
600
|
+
"endDate": HealthKitIdentifiers.isoString(from: statistics.endDate),
|
|
601
|
+
"unit": unit.unitString
|
|
602
|
+
]
|
|
603
|
+
record["sum"] = statistics.sumQuantity()?.doubleValue(for: unit)
|
|
604
|
+
record["min"] = statistics.minimumQuantity()?.doubleValue(for: unit)
|
|
605
|
+
record["max"] = statistics.maximumQuantity()?.doubleValue(for: unit)
|
|
606
|
+
record["average"] = statistics.averageQuantity()?.doubleValue(for: unit)
|
|
607
|
+
record["mostRecent"] = statistics.mostRecentQuantity()?.doubleValue(for: unit)
|
|
608
|
+
return record
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private func readCharacteristic(_ read: () throws -> Int) throws -> Int {
|
|
612
|
+
do {
|
|
613
|
+
return try read()
|
|
614
|
+
} catch {
|
|
615
|
+
if isNoData(error) {
|
|
616
|
+
return 0
|
|
617
|
+
}
|
|
618
|
+
throw error
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private func isNoData(_ error: Error) -> Bool {
|
|
623
|
+
let nsError = error as NSError
|
|
624
|
+
return nsError.domain == HKErrorDomain && nsError.code == HKError.Code.errorNoData.rawValue
|
|
625
|
+
}
|
|
626
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>NSPrivacyTracking</key>
|
|
6
|
+
<false/>
|
|
7
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
8
|
+
<array/>
|
|
9
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
10
|
+
<array/>
|
|
11
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
12
|
+
<array/>
|
|
13
|
+
</dict>
|
|
14
|
+
</plist>
|