@flomentumsolutions/capacitor-health-extended 0.0.7 → 0.0.9
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.
|
@@ -6,6 +6,7 @@ import HealthKit
|
|
|
6
6
|
* Please read the Capacitor iOS Plugin Development Guide
|
|
7
7
|
* here: https://capacitorjs.com/docs/plugins/ios
|
|
8
8
|
*/
|
|
9
|
+
@MainActor
|
|
9
10
|
@objc(HealthPlugin)
|
|
10
11
|
public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
12
|
public let identifier = "HealthPlugin"
|
|
@@ -22,6 +23,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
22
23
|
|
|
23
24
|
let healthStore = HKHealthStore()
|
|
24
25
|
|
|
26
|
+
/// Serial queue to make route‑location mutations thread‑safe without locks
|
|
27
|
+
private let routeSyncQueue = DispatchQueue(label: "com.flomentum.healthplugin.routeSync")
|
|
28
|
+
|
|
25
29
|
@objc func isHealthAvailable(_ call: CAPPluginCall) {
|
|
26
30
|
let isAvailable = HKHealthStore.isHealthDataAvailable()
|
|
27
31
|
call.resolve(["available": isAvailable])
|
|
@@ -296,42 +300,56 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
296
300
|
let bucket = call.getString("bucket"),
|
|
297
301
|
let startDate = self.isoDateFormatter.date(from: startDateString),
|
|
298
302
|
let endDate = self.isoDateFormatter.date(from: endDateString) else {
|
|
299
|
-
|
|
303
|
+
DispatchQueue.main.async {
|
|
304
|
+
call.reject("Invalid parameters")
|
|
305
|
+
}
|
|
300
306
|
return
|
|
301
307
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
308
|
+
if dataTypeString == "mindfulness" {
|
|
309
|
+
self.queryMindfulnessAggregated(startDate: startDate, endDate: endDate) { result, error in
|
|
310
|
+
DispatchQueue.main.async {
|
|
311
|
+
if let error = error {
|
|
312
|
+
call.reject(error.localizedDescription)
|
|
313
|
+
} else if let result = result {
|
|
314
|
+
call.resolve(["aggregatedData": result])
|
|
315
|
+
}
|
|
309
316
|
}
|
|
310
317
|
}
|
|
311
318
|
} else {
|
|
312
319
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
313
|
-
|
|
314
320
|
guard let interval = calculateInterval(bucket: bucket) else {
|
|
315
|
-
|
|
321
|
+
DispatchQueue.main.async {
|
|
322
|
+
call.reject("Invalid bucket")
|
|
323
|
+
}
|
|
316
324
|
return
|
|
317
325
|
}
|
|
318
|
-
|
|
319
326
|
guard let dataType = aggregateTypeToHKQuantityType(dataTypeString) else {
|
|
320
|
-
|
|
327
|
+
DispatchQueue.main.async {
|
|
328
|
+
call.reject("Invalid data type")
|
|
329
|
+
}
|
|
321
330
|
return
|
|
322
331
|
}
|
|
323
|
-
|
|
324
332
|
let options: HKStatisticsOptions = {
|
|
325
333
|
switch dataType.aggregationStyle {
|
|
326
334
|
case .cumulative:
|
|
327
335
|
return .cumulativeSum
|
|
336
|
+
case .discreteAverage:
|
|
337
|
+
return .discreteAverage
|
|
338
|
+
@available(iOS 17.0, *)
|
|
339
|
+
case .discreteTemporallyWeighted:
|
|
340
|
+
return .discreteAverage
|
|
341
|
+
@available(iOS 17.0, *)
|
|
342
|
+
case .discreteEquivalentContinuousLevel:
|
|
343
|
+
return .discreteAverage
|
|
344
|
+
@available(iOS 17.0, *)
|
|
345
|
+
case .discreteArithmetic:
|
|
346
|
+
return .discreteAverage
|
|
328
347
|
case .discrete:
|
|
329
|
-
return .discreteAverage
|
|
348
|
+
return .discreteAverage
|
|
330
349
|
@unknown default:
|
|
331
|
-
return .
|
|
350
|
+
return .discreteAverage
|
|
332
351
|
}
|
|
333
352
|
}()
|
|
334
|
-
|
|
335
353
|
let query = HKStatisticsCollectionQuery(
|
|
336
354
|
quantityType: dataType,
|
|
337
355
|
quantitySamplePredicate: predicate,
|
|
@@ -339,139 +357,107 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
339
357
|
anchorDate: startDate,
|
|
340
358
|
intervalComponents: interval
|
|
341
359
|
)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
378
|
-
case "hrv":
|
|
379
|
-
return HKUnit.secondUnit(with: .milli)
|
|
380
|
-
case "mindfulness":
|
|
381
|
-
return HKUnit.second()
|
|
382
|
-
default:
|
|
383
|
-
return .count()
|
|
384
|
-
}
|
|
385
|
-
}()
|
|
386
|
-
|
|
387
|
-
let value = quantity.doubleValue(for: unit)
|
|
388
|
-
|
|
389
|
-
aggregatedSamples.append([
|
|
390
|
-
"startDate": bucketStart,
|
|
391
|
-
"endDate": bucketEnd,
|
|
392
|
-
"value": value
|
|
393
|
-
])
|
|
360
|
+
query.initialResultsHandler = { _, result, error in
|
|
361
|
+
DispatchQueue.main.async {
|
|
362
|
+
if let error = error {
|
|
363
|
+
call.reject("Error fetching aggregated data: \(error.localizedDescription)")
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
var aggregatedSamples: [[String: Any]] = []
|
|
367
|
+
result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
|
|
368
|
+
let quantity: HKQuantity? = options.contains(.cumulativeSum)
|
|
369
|
+
? statistics.sumQuantity()
|
|
370
|
+
: statistics.averageQuantity()
|
|
371
|
+
guard let quantity = quantity else { return }
|
|
372
|
+
let bucketStart = statistics.startDate.timeIntervalSince1970 * 1000
|
|
373
|
+
let bucketEnd = statistics.endDate.timeIntervalSince1970 * 1000
|
|
374
|
+
let unit: HKUnit = {
|
|
375
|
+
switch dataTypeString {
|
|
376
|
+
case "steps": return .count()
|
|
377
|
+
case "active-calories", "total-calories": return .kilocalorie()
|
|
378
|
+
case "distance": return .meter()
|
|
379
|
+
case "weight": return .gramUnit(with: .kilo)
|
|
380
|
+
case "height": return .meter()
|
|
381
|
+
case "heart-rate": return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
382
|
+
case "hrv": return HKUnit.secondUnit(with: .milli)
|
|
383
|
+
case "mindfulness": return HKUnit.second()
|
|
384
|
+
default: return .count()
|
|
385
|
+
}
|
|
386
|
+
}()
|
|
387
|
+
let value = quantity.doubleValue(for: unit)
|
|
388
|
+
aggregatedSamples.append([
|
|
389
|
+
"startDate": bucketStart,
|
|
390
|
+
"endDate": bucketEnd,
|
|
391
|
+
"value": value
|
|
392
|
+
])
|
|
393
|
+
}
|
|
394
|
+
call.resolve(["aggregatedData": aggregatedSamples])
|
|
394
395
|
}
|
|
395
|
-
|
|
396
|
-
call.resolve(["aggregatedData": aggregatedSamples])
|
|
397
396
|
}
|
|
398
|
-
|
|
399
397
|
healthStore.execute(query)
|
|
400
398
|
}
|
|
401
399
|
}
|
|
402
|
-
|
|
400
|
+
|
|
403
401
|
func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping ([[String: Any]]?, Error?) -> Void) {
|
|
404
402
|
guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
|
|
405
|
-
|
|
403
|
+
DispatchQueue.main.async {
|
|
404
|
+
completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "MindfulSession type unavailable"]))
|
|
405
|
+
}
|
|
406
406
|
return
|
|
407
407
|
}
|
|
408
|
-
|
|
409
408
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
410
409
|
let query = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
|
|
411
|
-
guard let categorySamples = samples as? [HKCategorySample], error == nil else {
|
|
412
|
-
completion(nil, error)
|
|
413
|
-
return
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Aggregate total time per day
|
|
417
|
-
|
|
418
410
|
var dailyDurations: [Date: TimeInterval] = [:]
|
|
419
411
|
let calendar = Calendar.current
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
412
|
+
if let categorySamples = samples as? [HKCategorySample], error == nil {
|
|
413
|
+
for sample in categorySamples {
|
|
414
|
+
let startOfDay = calendar.startOfDay(for: sample.startDate)
|
|
415
|
+
let duration = sample.endDate.timeIntervalSince(sample.startDate)
|
|
416
|
+
dailyDurations[startOfDay, default: 0] += duration
|
|
417
|
+
}
|
|
418
|
+
var aggregatedSamples: [[String: Any]] = []
|
|
419
|
+
let dayComponent = DateComponents(day: 1)
|
|
420
|
+
for (date, duration) in dailyDurations {
|
|
421
|
+
aggregatedSamples.append([
|
|
422
|
+
"startDate": date,
|
|
423
|
+
"endDate": calendar.date(byAdding: dayComponent, to: date) as Any,
|
|
424
|
+
"value": duration
|
|
425
|
+
])
|
|
426
|
+
}
|
|
427
|
+
DispatchQueue.main.async {
|
|
428
|
+
completion(aggregatedSamples, nil)
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
DispatchQueue.main.async {
|
|
432
|
+
completion(nil, error)
|
|
429
433
|
}
|
|
430
434
|
}
|
|
431
|
-
|
|
432
|
-
var aggregatedSamples: [[String: Any]] = []
|
|
433
|
-
var dayComponent = DateComponents()
|
|
434
|
-
dayComponent.day = 1
|
|
435
|
-
dailyDurations.forEach { (dateAndDuration) in
|
|
436
|
-
aggregatedSamples.append([
|
|
437
|
-
"startDate": dateAndDuration.key as Any,
|
|
438
|
-
"endDate": calendar.date(byAdding: dayComponent, to: dateAndDuration.key) as Any,
|
|
439
|
-
"value": dateAndDuration.value
|
|
440
|
-
])
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
completion(aggregatedSamples, nil)
|
|
444
435
|
}
|
|
445
|
-
|
|
446
436
|
healthStore.execute(query)
|
|
447
437
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping(Double?) -> Void) {
|
|
452
|
-
|
|
453
|
-
|
|
438
|
+
|
|
439
|
+
private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping (Double?) -> Void) {
|
|
454
440
|
guard let quantityType = dataType else {
|
|
455
|
-
|
|
441
|
+
DispatchQueue.main.async {
|
|
442
|
+
completion(nil)
|
|
443
|
+
}
|
|
456
444
|
return
|
|
457
445
|
}
|
|
458
|
-
|
|
459
446
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
460
|
-
|
|
461
447
|
let query = HKStatisticsQuery(
|
|
462
448
|
quantityType: quantityType,
|
|
463
449
|
quantitySamplePredicate: predicate,
|
|
464
450
|
options: .cumulativeSum
|
|
465
451
|
) { _, result, _ in
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return
|
|
452
|
+
let value: Double? = {
|
|
453
|
+
guard let result = result, let sum = result.sumQuantity() else { return 0.0 }
|
|
454
|
+
return sum.doubleValue(for: HKUnit.count())
|
|
455
|
+
}()
|
|
456
|
+
DispatchQueue.main.async {
|
|
457
|
+
completion(value)
|
|
469
458
|
}
|
|
470
|
-
completion(sum.doubleValue(for: HKUnit.count()))
|
|
471
459
|
}
|
|
472
|
-
|
|
473
460
|
healthStore.execute(query)
|
|
474
|
-
|
|
475
461
|
}
|
|
476
462
|
|
|
477
463
|
|
|
@@ -509,30 +495,28 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
509
495
|
call.reject("Invalid parameters")
|
|
510
496
|
return
|
|
511
497
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// Create a predicate to filter workouts by date
|
|
498
|
+
|
|
516
499
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
517
|
-
|
|
518
|
-
|
|
500
|
+
let workoutQuery = HKSampleQuery(sampleType: HKObjectType.workoutType(), predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { [weak self] query, samples, error in
|
|
501
|
+
guard let self = self else { return }
|
|
519
502
|
if let error = error {
|
|
520
|
-
|
|
503
|
+
DispatchQueue.main.async {
|
|
504
|
+
call.reject("Error querying workouts: \(error.localizedDescription)")
|
|
505
|
+
}
|
|
521
506
|
return
|
|
522
507
|
}
|
|
523
|
-
|
|
524
508
|
guard let workouts = samples as? [HKWorkout] else {
|
|
525
|
-
|
|
509
|
+
DispatchQueue.main.async {
|
|
510
|
+
call.resolve(["workouts": []])
|
|
511
|
+
}
|
|
526
512
|
return
|
|
527
513
|
}
|
|
528
|
-
|
|
529
|
-
var
|
|
514
|
+
let outerGroup = DispatchGroup()
|
|
515
|
+
var workoutResults: [[String: Any]] = []
|
|
530
516
|
var errors: [String: String] = [:]
|
|
531
|
-
let dispatchGroup = DispatchGroup()
|
|
532
|
-
|
|
533
|
-
// Process each workout
|
|
534
517
|
for workout in workouts {
|
|
535
|
-
|
|
518
|
+
outerGroup.enter()
|
|
519
|
+
var localDict: [String: Any] = [
|
|
536
520
|
"startDate": workout.startDate,
|
|
537
521
|
"endDate": workout.endDate,
|
|
538
522
|
"workoutType": self.workoutTypeMapping[workout.workoutActivityType.rawValue, default: "other"],
|
|
@@ -543,66 +527,52 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
543
527
|
"calories": workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) ?? 0,
|
|
544
528
|
"distance": workout.totalDistance?.doubleValue(for: .meter()) ?? 0
|
|
545
529
|
]
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
var
|
|
549
|
-
var routeSamples: [[String: Any]] = []
|
|
550
|
-
|
|
551
|
-
// Query heart rate data if requested
|
|
530
|
+
let innerGroup = DispatchGroup()
|
|
531
|
+
var localHeartRates: [[String: Any]] = []
|
|
532
|
+
var localRoutes: [[String: Any]] = []
|
|
552
533
|
if includeHeartRate {
|
|
553
|
-
|
|
554
|
-
self.queryHeartRate(for: workout
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
dispatchGroup.leave()
|
|
560
|
-
})
|
|
534
|
+
innerGroup.enter()
|
|
535
|
+
self.queryHeartRate(for: workout) { rates, error in
|
|
536
|
+
localHeartRates = rates
|
|
537
|
+
if let error = error { errors["heart-rate"] = error }
|
|
538
|
+
innerGroup.leave()
|
|
539
|
+
}
|
|
561
540
|
}
|
|
562
|
-
|
|
563
|
-
// Query route data if requested
|
|
564
541
|
if includeRoute {
|
|
565
|
-
|
|
566
|
-
self.queryRoute(for: workout
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
dispatchGroup.leave()
|
|
572
|
-
})
|
|
542
|
+
innerGroup.enter()
|
|
543
|
+
self.queryRoute(for: workout) { routes, error in
|
|
544
|
+
localRoutes = routes
|
|
545
|
+
if let error = error { errors["route"] = error }
|
|
546
|
+
innerGroup.leave()
|
|
547
|
+
}
|
|
573
548
|
}
|
|
574
|
-
|
|
575
549
|
if includeSteps {
|
|
576
|
-
|
|
577
|
-
self.queryAggregated(for: workout.startDate, for: workout.endDate, for: HKObjectType.quantityType(forIdentifier: .stepCount)
|
|
578
|
-
if
|
|
579
|
-
|
|
550
|
+
innerGroup.enter()
|
|
551
|
+
self.queryAggregated(for: workout.startDate, for: workout.endDate, for: HKObjectType.quantityType(forIdentifier: .stepCount)) { steps in
|
|
552
|
+
if let steps = steps {
|
|
553
|
+
localDict["steps"] = steps
|
|
580
554
|
}
|
|
581
|
-
|
|
582
|
-
}
|
|
555
|
+
innerGroup.leave()
|
|
556
|
+
}
|
|
583
557
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
558
|
+
innerGroup.notify(queue: .main) {
|
|
559
|
+
localDict["heartRate"] = localHeartRates
|
|
560
|
+
localDict["route"] = localRoutes
|
|
561
|
+
workoutResults.append(localDict)
|
|
562
|
+
outerGroup.leave()
|
|
589
563
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
564
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
call.resolve(["workouts": workoutList, "errors": errors])
|
|
565
|
+
outerGroup.notify(queue: .main) {
|
|
566
|
+
call.resolve(["workouts": workoutResults, "errors": errors])
|
|
596
567
|
}
|
|
597
568
|
}
|
|
598
|
-
|
|
599
569
|
healthStore.execute(workoutQuery)
|
|
600
570
|
}
|
|
601
571
|
|
|
602
572
|
|
|
603
573
|
|
|
604
574
|
// MARK: - Query Heart Rate Data
|
|
605
|
-
private func queryHeartRate(for workout: HKWorkout, completion: @escaping ([[String: Any]], String?) -> Void) {
|
|
575
|
+
private func queryHeartRate(for workout: HKWorkout, completion: @escaping @Sendable ([[String: Any]], String?) -> Void) {
|
|
606
576
|
let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
|
|
607
577
|
let predicate = HKQuery.predicateForSamples(withStart: workout.startDate, end: workout.endDate, options: .strictStartDate)
|
|
608
578
|
|
|
@@ -633,30 +603,29 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
633
603
|
}
|
|
634
604
|
|
|
635
605
|
// MARK: - Query Route Data
|
|
636
|
-
private func queryRoute(for workout: HKWorkout, completion: @escaping ([[String: Any]], String?) -> Void) {
|
|
606
|
+
private func queryRoute(for workout: HKWorkout, completion: @escaping @Sendable ([[String: Any]], String?) -> Void) {
|
|
637
607
|
let routeType = HKSeriesType.workoutRoute()
|
|
638
608
|
let predicate = HKQuery.predicateForObjects(from: workout)
|
|
639
609
|
|
|
640
|
-
let routeQuery = HKSampleQuery(sampleType: routeType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) {
|
|
641
|
-
guard let
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
610
|
+
let routeQuery = HKSampleQuery(sampleType: routeType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { [weak self] _, samples, error in
|
|
611
|
+
guard let self = self else { return }
|
|
612
|
+
if let routes = samples as? [HKWorkoutRoute], error == nil {
|
|
613
|
+
let routeDispatchGroup = DispatchGroup()
|
|
614
|
+
var allLocations: [[String: Any]] = []
|
|
615
|
+
for route in routes {
|
|
616
|
+
routeDispatchGroup.enter()
|
|
617
|
+
self.queryLocations(for: route) { locations in
|
|
618
|
+
allLocations.append(contentsOf: locations)
|
|
619
|
+
routeDispatchGroup.leave()
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
routeDispatchGroup.notify(queue: .main) {
|
|
623
|
+
completion(allLocations, nil)
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
DispatchQueue.main.async {
|
|
627
|
+
completion([], error?.localizedDescription)
|
|
655
628
|
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
routeDispatchGroup.notify(queue: .main) {
|
|
659
|
-
completion(routeLocations, nil)
|
|
660
629
|
}
|
|
661
630
|
}
|
|
662
631
|
|
|
@@ -664,30 +633,38 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
664
633
|
}
|
|
665
634
|
|
|
666
635
|
// MARK: - Query Route Locations
|
|
667
|
-
private func queryLocations(for route: HKWorkoutRoute, completion: @escaping ([[String: Any]]) -> Void) {
|
|
636
|
+
private func queryLocations(for route: HKWorkoutRoute, completion: @escaping @Sendable ([[String: Any]]) -> Void) {
|
|
668
637
|
var routeLocations: [[String: Any]] = []
|
|
669
|
-
|
|
670
|
-
let locationQuery = HKWorkoutRouteQuery(route: route) {
|
|
638
|
+
|
|
639
|
+
let locationQuery = HKWorkoutRouteQuery(route: route) { [weak self] _, locations, done, error in
|
|
640
|
+
guard let self = self else { return }
|
|
671
641
|
guard let locations = locations, error == nil else {
|
|
672
|
-
|
|
642
|
+
DispatchQueue.main.async {
|
|
643
|
+
completion([])
|
|
644
|
+
}
|
|
673
645
|
return
|
|
674
646
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
647
|
+
|
|
648
|
+
// Append on a dedicated serial queue so we’re race‑free without NSLock
|
|
649
|
+
self.routeSyncQueue.async {
|
|
650
|
+
for location in locations {
|
|
651
|
+
let locationDict: [String: Any] = [
|
|
652
|
+
"timestamp": location.timestamp,
|
|
653
|
+
"lat": location.coordinate.latitude,
|
|
654
|
+
"lng": location.coordinate.longitude,
|
|
655
|
+
"alt": location.altitude
|
|
656
|
+
]
|
|
657
|
+
routeLocations.append(locationDict)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if done {
|
|
661
|
+
DispatchQueue.main.async {
|
|
662
|
+
completion(routeLocations)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
688
665
|
}
|
|
689
666
|
}
|
|
690
|
-
|
|
667
|
+
|
|
691
668
|
healthStore.execute(locationQuery)
|
|
692
669
|
}
|
|
693
670
|
|
package/package.json
CHANGED