@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +92 -0
  3. package/README.md +453 -0
  4. package/android/build.gradle +22 -0
  5. package/android/src/main/AndroidManifest.xml +74 -0
  6. package/android/src/main/java/expo/modules/healthkit/ExpoHealthKitModule.kt +228 -0
  7. package/android/src/main/java/expo/modules/healthkit/HealthConnectMapping.kt +129 -0
  8. package/android/src/main/java/expo/modules/healthkit/HealthConnectNutrition.kt +152 -0
  9. package/android/src/main/java/expo/modules/healthkit/HealthConnectRationaleActivity.kt +62 -0
  10. package/android/src/main/java/expo/modules/healthkit/HealthConnectService.kt +1054 -0
  11. package/android/src/main/java/expo/modules/healthkit/HealthConnectUnits.kt +133 -0
  12. package/android/src/main/java/expo/modules/healthkit/HealthConnectWorkouts.kt +105 -0
  13. package/android/src/main/java/expo/modules/healthkit/HealthKitExceptions.kt +30 -0
  14. package/app.plugin.js +1 -0
  15. package/build/ExpoHealthKitModule.d.ts +39 -0
  16. package/build/ExpoHealthKitModule.d.ts.map +1 -0
  17. package/build/ExpoHealthKitModule.js +35 -0
  18. package/build/ExpoHealthKitModule.js.map +1 -0
  19. package/build/ExpoHealthKitModule.web.d.ts +39 -0
  20. package/build/ExpoHealthKitModule.web.d.ts.map +1 -0
  21. package/build/ExpoHealthKitModule.web.js +105 -0
  22. package/build/ExpoHealthKitModule.web.js.map +1 -0
  23. package/build/dates.d.ts +6 -0
  24. package/build/dates.d.ts.map +1 -0
  25. package/build/dates.js +13 -0
  26. package/build/dates.js.map +1 -0
  27. package/build/errors.d.ts +9 -0
  28. package/build/errors.d.ts.map +1 -0
  29. package/build/errors.js +18 -0
  30. package/build/errors.js.map +1 -0
  31. package/build/identifiers.d.ts +418 -0
  32. package/build/identifiers.d.ts.map +1 -0
  33. package/build/identifiers.js +402 -0
  34. package/build/identifiers.js.map +1 -0
  35. package/build/index.d.ts +454 -0
  36. package/build/index.d.ts.map +1 -0
  37. package/build/index.js +418 -0
  38. package/build/index.js.map +1 -0
  39. package/build/types.d.ts +542 -0
  40. package/build/types.d.ts.map +1 -0
  41. package/build/types.js +2 -0
  42. package/build/types.js.map +1 -0
  43. package/docs/banner.jpg +0 -0
  44. package/expo-module.config.json +9 -0
  45. package/ios/ExpoHealthKit.podspec +30 -0
  46. package/ios/ExpoHealthKitModule.swift +155 -0
  47. package/ios/HealthKitAdvancedQueries.swift +458 -0
  48. package/ios/HealthKitExceptions.swift +109 -0
  49. package/ios/HealthKitIdentifiers.swift +183 -0
  50. package/ios/HealthKitRecords.swift +214 -0
  51. package/ios/HealthKitService.swift +626 -0
  52. package/ios/PrivacyInfo.xcprivacy +14 -0
  53. package/package.json +105 -0
  54. package/plugin/build/index.d.ts +30 -0
  55. package/plugin/build/index.js +138 -0
  56. package/src/ExpoHealthKitModule.ts +116 -0
  57. package/src/ExpoHealthKitModule.web.ts +140 -0
  58. package/src/dates.ts +17 -0
  59. package/src/errors.ts +22 -0
  60. package/src/identifiers.ts +494 -0
  61. package/src/index.ts +563 -0
  62. 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>