@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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
854
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
982
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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