@interval-health/capacitor-health 1.1.1 → 1.2.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.
@@ -351,9 +351,14 @@ final class Health {
351
351
 
352
352
  // For all other data types, use the standard query approach
353
353
  let sampleType = try dataType.sampleType()
354
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
354
+ // Use .strictStartDate to include samples that start within the range (even if they end after endDate)
355
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
355
356
  let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: ascending)
356
- let queryLimit = limit ?? 100
357
+
358
+ // Use no limit by default to ensure all samples are retrieved
359
+ // This is important for data types like sleep, heart rate, steps, etc.
360
+ // where a single day or long date ranges can have many samples
361
+ let queryLimit = limit ?? HKObjectQueryNoLimit
357
362
 
358
363
  let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: queryLimit, sortDescriptors: [sortDescriptor]) { [weak self] _, samples, error in
359
364
  guard let self = self else { return }
@@ -842,16 +847,23 @@ final class Health {
842
847
  }
843
848
 
844
849
  private func processSleepSamples(_ samples: [HKCategorySample]) -> [[String: Any]] {
845
- // Filter for detailed stage data only (Deep, REM, Core, Awake)
850
+ // Filter for sleep stage data - include both detailed stages (iOS 16+) and legacy values
851
+ // This ensures we capture data from all sources (Apple Watch, third-party apps, etc.)
846
852
  let detailedSamples = samples.filter { sample in
847
853
  if #available(iOS 16.0, *) {
854
+ // Include all sleep-related values:
855
+ // - Detailed stages: asleepDeep, asleepREM, asleepCore, awake (iOS 16+)
856
+ // - Legacy/basic: asleepUnspecified, asleep (for older data or third-party apps)
857
+ // - Exclude: inBed (not actual sleep, just time in bed)
848
858
  return sample.value == HKCategoryValueSleepAnalysis.asleepDeep.rawValue ||
849
859
  sample.value == HKCategoryValueSleepAnalysis.asleepREM.rawValue ||
850
860
  sample.value == HKCategoryValueSleepAnalysis.asleepCore.rawValue ||
851
- sample.value == HKCategoryValueSleepAnalysis.awake.rawValue
861
+ sample.value == HKCategoryValueSleepAnalysis.awake.rawValue ||
862
+ sample.value == HKCategoryValueSleepAnalysis.asleepUnspecified.rawValue
852
863
  } else {
853
- // For older iOS, just process what's available
854
- return true
864
+ // For older iOS, include asleep and awake, exclude inBed
865
+ return sample.value == HKCategoryValueSleepAnalysis.asleep.rawValue ||
866
+ sample.value == HKCategoryValueSleepAnalysis.awake.rawValue
855
867
  }
856
868
  }
857
869
 
@@ -927,6 +939,7 @@ final class Health {
927
939
  var remMinutes: Double
928
940
  var coreMinutes: Double
929
941
  var awakeMinutes: Double
942
+ var unspecifiedMinutes: Double // For legacy/third-party sleep data without stage details
930
943
  }
931
944
 
932
945
  var sleepByDate: [String: DayData] = [:]
@@ -944,7 +957,8 @@ final class Health {
944
957
  deepMinutes: 0,
945
958
  remMinutes: 0,
946
959
  coreMinutes: 0,
947
- awakeMinutes: 0
960
+ awakeMinutes: 0,
961
+ unspecifiedMinutes: 0
948
962
  )
949
963
  }
950
964
 
@@ -962,6 +976,10 @@ final class Health {
962
976
  sleepByDate[dateString]!.coreMinutes += minutes
963
977
  } else if segment.stage == "HKCategoryValueSleepAnalysisAwake" {
964
978
  sleepByDate[dateString]!.awakeMinutes += minutes
979
+ } else if segment.stage == "HKCategoryValueSleepAnalysisAsleepUnspecified" ||
980
+ segment.stage == "HKCategoryValueSleepAnalysisAsleep" {
981
+ // Legacy sleep data or third-party apps that don't provide stage details
982
+ sleepByDate[dateString]!.unspecifiedMinutes += minutes
965
983
  }
966
984
  }
967
985
  }
@@ -974,14 +992,15 @@ final class Health {
974
992
  let remHours = data.remMinutes / 60.0
975
993
  let coreHours = data.coreMinutes / 60.0
976
994
  let awakeHours = data.awakeMinutes / 60.0
977
- let totalSleepHours = deepHours + remHours + coreHours
995
+ let unspecifiedHours = data.unspecifiedMinutes / 60.0
996
+ // Include unspecified sleep in total (this captures legacy/third-party app data)
997
+ let totalSleepHours = deepHours + remHours + coreHours + unspecifiedHours
978
998
 
979
- // Calculate time in bed from merged sessions (first start to last end)
999
+ // Calculate time in bed by summing each session's duration
1000
+ // This avoids counting gaps between sessions (e.g., nap + main sleep)
980
1001
  var timeInBed = 0.0
981
- if !data.sessions.isEmpty {
982
- let bedtime = data.sessions.first!.start
983
- let wakeTime = data.sessions.last!.end
984
- timeInBed = wakeTime.timeIntervalSince(bedtime) / 3600.0
1002
+ for session in data.sessions {
1003
+ timeInBed += session.end.timeIntervalSince(session.start) / 3600.0
985
1004
  }
986
1005
 
987
1006
  // Calculate efficiency
@@ -1011,6 +1030,7 @@ final class Health {
1011
1030
  "deepSleep": round(deepHours * 10) / 10,
1012
1031
  "remSleep": round(remHours * 10) / 10,
1013
1032
  "coreSleep": round(coreHours * 10) / 10,
1033
+ "unspecifiedSleep": round(unspecifiedHours * 10) / 10, // Legacy/third-party sleep without stage details
1014
1034
  "awakeTime": round(awakeHours * 10) / 10,
1015
1035
  "timeInBed": round(timeInBed * 10) / 10,
1016
1036
  "efficiency": efficiency,
@@ -1403,7 +1423,7 @@ final class Health {
1403
1423
 
1404
1424
  group.enter()
1405
1425
 
1406
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1426
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1407
1427
  let query = HKSampleQuery(sampleType: quantityType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1408
1428
  defer { group.leave() }
1409
1429
 
@@ -1448,7 +1468,7 @@ final class Health {
1448
1468
  lock.unlock()
1449
1469
 
1450
1470
  case .walkingAsymmetryPercentage:
1451
- // Convert to percentage (HealthKit stores as decimal)
1471
+ // HKUnit.percent() returns fractional value (0.05 = 5%), multiply by 100 for percentage
1452
1472
  value = sample.quantity.doubleValue(for: HKUnit.percent()) * 100
1453
1473
  lock.lock()
1454
1474
  if asymmetryMap[dateString] == nil {
@@ -1458,7 +1478,7 @@ final class Health {
1458
1478
  lock.unlock()
1459
1479
 
1460
1480
  case .walkingDoubleSupportPercentage:
1461
- // Convert to percentage (HealthKit stores as decimal)
1481
+ // HKUnit.percent() returns fractional value (0.30 = 30%), multiply by 100 for percentage
1462
1482
  value = sample.quantity.doubleValue(for: HKUnit.percent()) * 100
1463
1483
  lock.lock()
1464
1484
  if doubleSupportMap[dateString] == nil {
@@ -1587,7 +1607,7 @@ final class Health {
1587
1607
  // === STEPS ===
1588
1608
  if let stepsType = HKObjectType.quantityType(forIdentifier: .stepCount) {
1589
1609
  group.enter()
1590
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1610
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1591
1611
  let query = HKSampleQuery(sampleType: stepsType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1592
1612
  defer { group.leave() }
1593
1613
 
@@ -1616,7 +1636,7 @@ final class Health {
1616
1636
  // === DISTANCE (Walking + Running) ===
1617
1637
  if let distanceType = HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning) {
1618
1638
  group.enter()
1619
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1639
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1620
1640
  let query = HKSampleQuery(sampleType: distanceType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1621
1641
  defer { group.leave() }
1622
1642
 
@@ -1647,7 +1667,7 @@ final class Health {
1647
1667
  // === FLIGHTS CLIMBED ===
1648
1668
  if let flightsType = HKObjectType.quantityType(forIdentifier: .flightsClimbed) {
1649
1669
  group.enter()
1650
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1670
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1651
1671
  let query = HKSampleQuery(sampleType: flightsType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1652
1672
  defer { group.leave() }
1653
1673
 
@@ -1676,7 +1696,7 @@ final class Health {
1676
1696
  // === ACTIVE ENERGY ===
1677
1697
  if let activeEnergyType = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned) {
1678
1698
  group.enter()
1679
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1699
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1680
1700
  let query = HKSampleQuery(sampleType: activeEnergyType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1681
1701
  defer { group.leave() }
1682
1702
 
@@ -1705,7 +1725,7 @@ final class Health {
1705
1725
  // === EXERCISE MINUTES ===
1706
1726
  if let exerciseType = HKObjectType.quantityType(forIdentifier: .appleExerciseTime) {
1707
1727
  group.enter()
1708
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1728
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1709
1729
  let query = HKSampleQuery(sampleType: exerciseType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1710
1730
  defer { group.leave() }
1711
1731
 
@@ -1734,7 +1754,7 @@ final class Health {
1734
1754
  // === STAND HOURS ===
1735
1755
  if let standHourType = HKObjectType.categoryType(forIdentifier: .appleStandHour) {
1736
1756
  group.enter()
1737
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1757
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1738
1758
  let query = HKSampleQuery(sampleType: standHourType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1739
1759
  defer { group.leave() }
1740
1760
 
@@ -1862,7 +1882,7 @@ final class Health {
1862
1882
  // === HEART RATE ===
1863
1883
  if let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate) {
1864
1884
  group.enter()
1865
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1885
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1866
1886
  let query = HKSampleQuery(sampleType: heartRateType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1867
1887
  defer { group.leave() }
1868
1888
 
@@ -1896,7 +1916,7 @@ final class Health {
1896
1916
  // === RESTING HEART RATE ===
1897
1917
  if let restingHRType = HKObjectType.quantityType(forIdentifier: .restingHeartRate) {
1898
1918
  group.enter()
1899
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1919
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1900
1920
  let query = HKSampleQuery(sampleType: restingHRType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1901
1921
  defer { group.leave() }
1902
1922
 
@@ -1925,7 +1945,7 @@ final class Health {
1925
1945
  // === VO2 MAX ===
1926
1946
  if let vo2MaxType = HKObjectType.quantityType(forIdentifier: .vo2Max) {
1927
1947
  group.enter()
1928
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1948
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1929
1949
  let query = HKSampleQuery(sampleType: vo2MaxType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1930
1950
  defer { group.leave() }
1931
1951
 
@@ -1954,7 +1974,7 @@ final class Health {
1954
1974
  // === HRV (Heart Rate Variability SDNN) ===
1955
1975
  if let hrvType = HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN) {
1956
1976
  group.enter()
1957
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
1977
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1958
1978
  let query = HKSampleQuery(sampleType: hrvType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1959
1979
  defer { group.leave() }
1960
1980
 
@@ -1983,7 +2003,7 @@ final class Health {
1983
2003
  // === SPO2 (Blood Oxygen Saturation) ===
1984
2004
  if let spo2Type = HKObjectType.quantityType(forIdentifier: .oxygenSaturation) {
1985
2005
  group.enter()
1986
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
2006
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
1987
2007
  let query = HKSampleQuery(sampleType: spo2Type, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
1988
2008
  defer { group.leave() }
1989
2009
 
@@ -2000,7 +2020,7 @@ final class Health {
2000
2020
  let dateComponents = calendar.dateComponents([.year, .month, .day], from: sample.startDate)
2001
2021
  let dateString = String(format: "%04d-%02d-%02d", dateComponents.year!, dateComponents.month!, dateComponents.day!)
2002
2022
 
2003
- // HealthKit stores as decimal (0.96), convert to percentage (96)
2023
+ // HKUnit.percent() returns fractional value (0.98 = 98%), multiply by 100 for percentage
2004
2024
  let value = sample.quantity.doubleValue(for: HKUnit.percent()) * 100
2005
2025
  lock.lock()
2006
2026
  if spo2Map[dateString] == nil {
@@ -2016,7 +2036,7 @@ final class Health {
2016
2036
  // === RESPIRATION RATE ===
2017
2037
  if let respirationRateType = HKObjectType.quantityType(forIdentifier: .respiratoryRate) {
2018
2038
  group.enter()
2019
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
2039
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
2020
2040
  let query = HKSampleQuery(sampleType: respirationRateType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
2021
2041
  defer { group.leave() }
2022
2042
 
@@ -2125,7 +2145,7 @@ final class Health {
2125
2145
 
2126
2146
  private func processWorkoutData(startDate: Date, endDate: Date, limit: Int?, ascending: Bool, completion: @escaping (Result<[[String: Any]], Error>) -> Void) {
2127
2147
  let workoutType = HKObjectType.workoutType()
2128
- let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
2148
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
2129
2149
  let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: ascending)
2130
2150
  let queryLimit = limit ?? HKObjectQueryNoLimit
2131
2151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interval-health/capacitor-health",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Capacitor plugin to interact with data from Apple HealthKit and Health Connect",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",