@flomentumsolutions/capacitor-health-extended 0.0.8 → 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.
@@ -16,5 +16,5 @@ Pod::Spec.new do |s|
16
16
  s.dependency 'Capacitor', '~> 6.2'
17
17
  s.dependency 'CapacitorCordova', '~> 6.2'
18
18
  # Match the Swift shipped with Xcode 16 (use 5.9 for Xcode 15.x)
19
- s.swift_version = '6.0'
19
+ s.swift_version = '5.9'
20
20
  end
@@ -300,37 +300,39 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
300
300
  let bucket = call.getString("bucket"),
301
301
  let startDate = self.isoDateFormatter.date(from: startDateString),
302
302
  let endDate = self.isoDateFormatter.date(from: endDateString) else {
303
- call.reject("Invalid parameters")
303
+ DispatchQueue.main.async {
304
+ call.reject("Invalid parameters")
305
+ }
304
306
  return
305
307
  }
306
-
307
- if(dataTypeString == "mindfulness") {
308
- self.queryMindfulnessAggregated(startDate: startDate, endDate: endDate) {result, error in
309
- if let error = error {
310
- call.reject(error.localizedDescription)
311
- } else if let result = result {
312
- call.resolve(["aggregatedData": result])
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
+ }
313
316
  }
314
317
  }
315
318
  } else {
316
319
  let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
317
-
318
320
  guard let interval = calculateInterval(bucket: bucket) else {
319
- call.reject("Invalid bucket")
321
+ DispatchQueue.main.async {
322
+ call.reject("Invalid bucket")
323
+ }
320
324
  return
321
325
  }
322
-
323
326
  guard let dataType = aggregateTypeToHKQuantityType(dataTypeString) else {
324
- call.reject("Invalid data type")
327
+ DispatchQueue.main.async {
328
+ call.reject("Invalid data type")
329
+ }
325
330
  return
326
331
  }
327
-
328
332
  let options: HKStatisticsOptions = {
329
333
  switch dataType.aggregationStyle {
330
334
  case .cumulative:
331
335
  return .cumulativeSum
332
-
333
- // Newer discrete aggregation styles (iOS 15 +)
334
336
  case .discreteAverage:
335
337
  return .discreteAverage
336
338
  @available(iOS 17.0, *)
@@ -342,16 +344,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
342
344
  @available(iOS 17.0, *)
343
345
  case .discreteArithmetic:
344
346
  return .discreteAverage
345
-
346
- // Legacy discrete fallback
347
347
  case .discrete:
348
348
  return .discreteAverage
349
-
350
349
  @unknown default:
351
350
  return .discreteAverage
352
351
  }
353
352
  }()
354
-
355
353
  let query = HKStatisticsCollectionQuery(
356
354
  quantityType: dataType,
357
355
  quantitySamplePredicate: predicate,
@@ -359,139 +357,107 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
359
357
  anchorDate: startDate,
360
358
  intervalComponents: interval
361
359
  )
362
-
363
- query.initialResultsHandler = { query, result, error in
364
- if let error = error {
365
- call.reject("Error fetching aggregated data: \(error.localizedDescription)")
366
- return
367
- }
368
-
369
- var aggregatedSamples: [[String: Any]] = []
370
-
371
- result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
372
- // Choose sum or average based on the options we picked
373
- let quantity: HKQuantity? = options.contains(.cumulativeSum)
374
- ? statistics.sumQuantity()
375
- : statistics.averageQuantity()
376
-
377
- guard let quantity = quantity else { return }
378
-
379
- // Time‑bounds of this bucket
380
- let bucketStart = statistics.startDate.timeIntervalSince1970 * 1000
381
- let bucketEnd = statistics.endDate.timeIntervalSince1970 * 1000
382
-
383
- // Map dataType correct HKUnit
384
- let unit: HKUnit = {
385
- switch dataTypeString {
386
- case "steps":
387
- return .count()
388
- case "active-calories", "total-calories":
389
- return .kilocalorie()
390
- case "distance":
391
- return .meter()
392
- case "weight":
393
- return .gramUnit(with: .kilo)
394
- case "height":
395
- return .meter()
396
- case "heart-rate":
397
- return HKUnit.count().unitDivided(by: HKUnit.minute())
398
- case "hrv":
399
- return HKUnit.secondUnit(with: .milli)
400
- case "mindfulness":
401
- return HKUnit.second()
402
- default:
403
- return .count()
404
- }
405
- }()
406
-
407
- let value = quantity.doubleValue(for: unit)
408
-
409
- aggregatedSamples.append([
410
- "startDate": bucketStart,
411
- "endDate": bucketEnd,
412
- "value": value
413
- ])
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])
414
395
  }
415
-
416
- call.resolve(["aggregatedData": aggregatedSamples])
417
396
  }
418
-
419
397
  healthStore.execute(query)
420
398
  }
421
399
  }
422
-
423
- func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping @Sendable ([[String: Any]]?, Error?) -> Void) {
400
+
401
+ func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping ([[String: Any]]?, Error?) -> Void) {
424
402
  guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
425
- completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "MindfulSession type unavailable"]))
403
+ DispatchQueue.main.async {
404
+ completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "MindfulSession type unavailable"]))
405
+ }
426
406
  return
427
407
  }
428
-
429
408
  let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
430
409
  let query = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
431
- guard let categorySamples = samples as? [HKCategorySample], error == nil else {
432
- completion(nil, error)
433
- return
434
- }
435
-
436
- // Aggregate total time per day
437
-
438
410
  var dailyDurations: [Date: TimeInterval] = [:]
439
411
  let calendar = Calendar.current
440
-
441
- for sample in categorySamples {
442
- let startOfDay = calendar.startOfDay(for: sample.startDate)
443
- let duration = sample.endDate.timeIntervalSince(sample.startDate)
444
-
445
- if let existingDuration = dailyDurations[startOfDay] {
446
- dailyDurations[startOfDay] = existingDuration + duration
447
- } else {
448
- dailyDurations[startOfDay] = duration
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)
449
433
  }
450
434
  }
451
-
452
- var aggregatedSamples: [[String: Any]] = []
453
- var dayComponent = DateComponents()
454
- dayComponent.day = 1
455
- dailyDurations.forEach { (dateAndDuration) in
456
- aggregatedSamples.append([
457
- "startDate": dateAndDuration.key as Any,
458
- "endDate": calendar.date(byAdding: dayComponent, to: dateAndDuration.key) as Any,
459
- "value": dateAndDuration.value
460
- ])
461
- }
462
-
463
- completion(aggregatedSamples, nil)
464
435
  }
465
-
466
436
  healthStore.execute(query)
467
437
  }
468
-
469
-
470
-
471
- private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping @Sendable(Double?) -> Void) {
472
-
473
-
438
+
439
+ private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping (Double?) -> Void) {
474
440
  guard let quantityType = dataType else {
475
- completion(nil)
441
+ DispatchQueue.main.async {
442
+ completion(nil)
443
+ }
476
444
  return
477
445
  }
478
-
479
446
  let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
480
-
481
447
  let query = HKStatisticsQuery(
482
448
  quantityType: quantityType,
483
449
  quantitySamplePredicate: predicate,
484
450
  options: .cumulativeSum
485
451
  ) { _, result, _ in
486
- guard let result = result, let sum = result.sumQuantity() else {
487
- completion(0.0)
488
- 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)
489
458
  }
490
- completion(sum.doubleValue(for: HKUnit.count()))
491
459
  }
492
-
493
460
  healthStore.execute(query)
494
-
495
461
  }
496
462
 
497
463
 
@@ -529,30 +495,28 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
529
495
  call.reject("Invalid parameters")
530
496
  return
531
497
  }
532
-
533
-
534
-
535
- // Create a predicate to filter workouts by date
498
+
536
499
  let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
537
-
538
- let workoutQuery = HKSampleQuery(sampleType: HKObjectType.workoutType(), predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { query, samples, error in
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 }
539
502
  if let error = error {
540
- call.reject("Error querying workouts: \(error.localizedDescription)")
503
+ DispatchQueue.main.async {
504
+ call.reject("Error querying workouts: \(error.localizedDescription)")
505
+ }
541
506
  return
542
507
  }
543
-
544
508
  guard let workouts = samples as? [HKWorkout] else {
545
- call.resolve(["workouts": []])
509
+ DispatchQueue.main.async {
510
+ call.resolve(["workouts": []])
511
+ }
546
512
  return
547
513
  }
548
-
549
- var workoutList: [[String: Any]] = []
514
+ let outerGroup = DispatchGroup()
515
+ var workoutResults: [[String: Any]] = []
550
516
  var errors: [String: String] = [:]
551
- let dispatchGroup = DispatchGroup()
552
-
553
- // Process each workout
554
517
  for workout in workouts {
555
- var workoutDict: [String: Any] = [
518
+ outerGroup.enter()
519
+ var localDict: [String: Any] = [
556
520
  "startDate": workout.startDate,
557
521
  "endDate": workout.endDate,
558
522
  "workoutType": self.workoutTypeMapping[workout.workoutActivityType.rawValue, default: "other"],
@@ -563,59 +527,45 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
563
527
  "calories": workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) ?? 0,
564
528
  "distance": workout.totalDistance?.doubleValue(for: .meter()) ?? 0
565
529
  ]
566
-
567
-
568
- var heartRateSamples: [[String: Any]] = []
569
- var routeSamples: [[String: Any]] = []
570
-
571
- // Query heart rate data if requested
530
+ let innerGroup = DispatchGroup()
531
+ var localHeartRates: [[String: Any]] = []
532
+ var localRoutes: [[String: Any]] = []
572
533
  if includeHeartRate {
573
- dispatchGroup.enter()
574
- self.queryHeartRate(for: workout, completion: { (heartRates, error) in
575
- if(error != nil) {
576
- errors["heart-rate"] = error
577
- }
578
- heartRateSamples = heartRates
579
- dispatchGroup.leave()
580
- })
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
+ }
581
540
  }
582
-
583
- // Query route data if requested
584
541
  if includeRoute {
585
- dispatchGroup.enter()
586
- self.queryRoute(for: workout, completion: { (routes, error) in
587
- if(error != nil) {
588
- errors["route"] = error
589
- }
590
- routeSamples = routes
591
- dispatchGroup.leave()
592
- })
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
+ }
593
548
  }
594
-
595
549
  if includeSteps {
596
- dispatchGroup.enter()
597
- self.queryAggregated(for: workout.startDate, for: workout.endDate, for: HKObjectType.quantityType(forIdentifier: .stepCount), completion:{ (steps) in
598
- if(steps != nil) {
599
- workoutDict["steps"] = steps
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
600
554
  }
601
- dispatchGroup.leave()
602
- })
555
+ innerGroup.leave()
556
+ }
603
557
  }
604
-
605
- dispatchGroup.notify(queue: .main) {
606
- workoutDict["heartRate"] = heartRateSamples
607
- workoutDict["route"] = routeSamples
608
- workoutList.append(workoutDict)
558
+ innerGroup.notify(queue: .main) {
559
+ localDict["heartRate"] = localHeartRates
560
+ localDict["route"] = localRoutes
561
+ workoutResults.append(localDict)
562
+ outerGroup.leave()
609
563
  }
610
-
611
-
612
564
  }
613
-
614
- dispatchGroup.notify(queue: .main) {
615
- call.resolve(["workouts": workoutList, "errors": errors])
565
+ outerGroup.notify(queue: .main) {
566
+ call.resolve(["workouts": workoutResults, "errors": errors])
616
567
  }
617
568
  }
618
-
619
569
  healthStore.execute(workoutQuery)
620
570
  }
621
571
 
@@ -657,26 +607,25 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
657
607
  let routeType = HKSeriesType.workoutRoute()
658
608
  let predicate = HKQuery.predicateForObjects(from: workout)
659
609
 
660
- let routeQuery = HKSampleQuery(sampleType: routeType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { query, samples, error in
661
- guard let routes = samples as? [HKWorkoutRoute], error == nil else {
662
- completion([], error?.localizedDescription)
663
- return
664
- }
665
-
666
- var routeLocations: [[String: Any]] = []
667
- let routeDispatchGroup = DispatchGroup()
668
-
669
- // Query locations for each route
670
- for route in routes {
671
- routeDispatchGroup.enter()
672
- self.queryLocations(for: route) { locations in
673
- routeLocations.append(contentsOf: locations)
674
- routeDispatchGroup.leave()
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)
675
628
  }
676
- }
677
-
678
- routeDispatchGroup.notify(queue: .main) {
679
- completion(routeLocations, nil)
680
629
  }
681
630
  }
682
631
 
@@ -687,9 +636,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
687
636
  private func queryLocations(for route: HKWorkoutRoute, completion: @escaping @Sendable ([[String: Any]]) -> Void) {
688
637
  var routeLocations: [[String: Any]] = []
689
638
 
690
- let locationQuery = HKWorkoutRouteQuery(route: route) { _, locations, done, error in
639
+ let locationQuery = HKWorkoutRouteQuery(route: route) { [weak self] _, locations, done, error in
640
+ guard let self = self else { return }
691
641
  guard let locations = locations, error == nil else {
692
- completion([])
642
+ DispatchQueue.main.async {
643
+ completion([])
644
+ }
693
645
  return
694
646
  }
695
647
 
@@ -706,7 +658,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
706
658
  }
707
659
 
708
660
  if done {
709
- completion(routeLocations)
661
+ DispatchQueue.main.async {
662
+ completion(routeLocations)
663
+ }
710
664
  }
711
665
  }
712
666
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomentumsolutions/capacitor-health-extended",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Capacitor plugin for Apple HealthKit and Google Health Connect Platform",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",