@craft-native/ios 0.0.290 → 0.0.293
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.
- package/README.md +5 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +20 -0
- package/templates/CraftHealth.swift +76 -0
package/README.md
CHANGED
|
@@ -569,6 +569,11 @@ const readings = await health.getHeartRateSamples({ startDate: Date.now() - 8640
|
|
|
569
569
|
const nights = await health.getSleepIntervals({ startDate: Date.now() - 86400000 })
|
|
570
570
|
// An app built before these reads existed has neither; check first.
|
|
571
571
|
if (health.supports('getHeartRateSamples')) { /* ... */ }
|
|
572
|
+
// Diabetes: glucose (mg/dL), insulin (IU, each marked basal or bolus) and carbs (g),
|
|
573
|
+
// one sample at a time with its UUID, oldest first. Ask for them read only.
|
|
574
|
+
await health.requestAuthorization(['bloodGlucose', 'insulinDelivery', 'dietaryCarbohydrates'], { write: false })
|
|
575
|
+
const glucose = await health.getSamples('bloodGlucose', { startDate: Date.now() - 86400000 })
|
|
576
|
+
const doses = await health.getSamples('insulinDelivery', { startDate: Date.now() - 86400000 })
|
|
572
577
|
// Mindful minutes: ask for 'mindful' (with writing allowed), then save a
|
|
573
578
|
// meditation or breathing session. The id is kept as the sample's external
|
|
574
579
|
// UUID, so saving it again keeps the first sample ({ existing: true }).
|
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -3116,6 +3116,22 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3116
3116
|
} else {
|
|
3117
3117
|
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
3118
3118
|
}
|
|
3119
|
+
// Glucose, insulin and carbs, one sample at a time (CraftHealth.swift).
|
|
3120
|
+
case "getHealthSamples":
|
|
3121
|
+
if config.enableHealthKit {
|
|
3122
|
+
if let dataType = body["type"] as? String {
|
|
3123
|
+
CraftHealth.shared.getSamples(
|
|
3124
|
+
type: dataType,
|
|
3125
|
+
startDate: body["startDate"] as? Double,
|
|
3126
|
+
endDate: body["endDate"] as? Double,
|
|
3127
|
+
limit: body["limit"] as? Int
|
|
3128
|
+
) { [weak self] in self?.answer(callbackId, $0) }
|
|
3129
|
+
} else {
|
|
3130
|
+
rejectCallback(callbackId, error: "getHealthSamples was called without a type", code: "INVALID_ARGUMENT")
|
|
3131
|
+
}
|
|
3132
|
+
} else {
|
|
3133
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
3134
|
+
}
|
|
3119
3135
|
case "getHealthSleepIntervals":
|
|
3120
3136
|
if config.enableHealthKit {
|
|
3121
3137
|
getHealthSleepIntervals(
|
|
@@ -5749,6 +5765,10 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5749
5765
|
options = options || {};
|
|
5750
5766
|
return craft._invoke('getHealthHeartRateSamples', {startDate: options.startDate, endDate: options.endDate, limit: options.limit});
|
|
5751
5767
|
},
|
|
5768
|
+
getSamples: function(type, options) {
|
|
5769
|
+
options = options || {};
|
|
5770
|
+
return craft._invoke('getHealthSamples', {type: type, startDate: options.startDate, endDate: options.endDate, limit: options.limit});
|
|
5771
|
+
},
|
|
5752
5772
|
getSleepIntervals: function(options) {
|
|
5753
5773
|
options = options || {};
|
|
5754
5774
|
return craft._invoke('getHealthSleepIntervals', {startDate: options.startDate, endDate: options.endDate});
|
|
@@ -106,6 +106,9 @@ final class CraftHealth {
|
|
|
106
106
|
getDailyStatistics(type: type, startDate: options(1)["startDate"] as? Double, endDate: options(1)["endDate"] as? Double, completion: completion)
|
|
107
107
|
case "getHeartRateSamples":
|
|
108
108
|
getHeartRateSamples(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, limit: options(0)["limit"] as? Int, completion: completion)
|
|
109
|
+
case "getSamples":
|
|
110
|
+
guard let type = args.first as? String else { return completion(.failure(CraftHealthError(message: "getSamples needs a type", code: "INVALID_ARGUMENT"))) }
|
|
111
|
+
getSamples(type: type, startDate: options(1)["startDate"] as? Double, endDate: options(1)["endDate"] as? Double, limit: options(1)["limit"] as? Int, completion: completion)
|
|
109
112
|
case "getSleepIntervals":
|
|
110
113
|
getSleepIntervals(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, completion: completion)
|
|
111
114
|
case "saveMindfulSession":
|
|
@@ -196,6 +199,12 @@ final class CraftHealth {
|
|
|
196
199
|
readTypes.insert(mindfulType)
|
|
197
200
|
shareTypes.insert(mindfulType)
|
|
198
201
|
}
|
|
202
|
+
case "bloodGlucose", "insulinDelivery":
|
|
203
|
+
// A CGM, meter, pump or closed loop's own writes: read
|
|
204
|
+
// only, never written back, so the sheet only asks to read.
|
|
205
|
+
if let diabetesType = Self.healthQuantity(type)?.type {
|
|
206
|
+
readTypes.insert(diabetesType)
|
|
207
|
+
}
|
|
199
208
|
case "dietaryEnergy", "dietaryProtein", "dietaryCarbohydrates", "dietaryFat":
|
|
200
209
|
// What a food log (MyFitnessPal, Lose It!, Cronometer...)
|
|
201
210
|
// writes to Apple Health. Request the write grant too unless
|
|
@@ -404,6 +413,8 @@ final class CraftHealth {
|
|
|
404
413
|
case "dietaryProtein": return (HKQuantityType.quantityType(forIdentifier: .dietaryProtein), .gram(), .cumulativeSum)
|
|
405
414
|
case "dietaryCarbohydrates": return (HKQuantityType.quantityType(forIdentifier: .dietaryCarbohydrates), .gram(), .cumulativeSum)
|
|
406
415
|
case "dietaryFat": return (HKQuantityType.quantityType(forIdentifier: .dietaryFatTotal), .gram(), .cumulativeSum)
|
|
416
|
+
case "bloodGlucose": return (HKQuantityType.quantityType(forIdentifier: .bloodGlucose), HKUnit(from: "mg/dL"), .discreteAverage)
|
|
417
|
+
case "insulinDelivery": return (HKQuantityType.quantityType(forIdentifier: .insulinDelivery), .internationalUnit(), .cumulativeSum)
|
|
407
418
|
default: return nil
|
|
408
419
|
}
|
|
409
420
|
}
|
|
@@ -613,6 +624,71 @@ final class CraftHealth {
|
|
|
613
624
|
healthStore.execute(query)
|
|
614
625
|
}
|
|
615
626
|
|
|
627
|
+
/// The types `getSamples` reads, and the unit each answers in.
|
|
628
|
+
private static let sampleUnits: [String: String] = ["bloodGlucose": "mg/dL", "insulinDelivery": "IU", "dietaryCarbohydrates": "g"]
|
|
629
|
+
|
|
630
|
+
/// Every glucose reading, insulin dose or carb entry between two
|
|
631
|
+
/// instants, oldest first, as the writing app saved it: its UUID (so a
|
|
632
|
+
/// sync stores it once), value, source and device, plus what a diabetes
|
|
633
|
+
/// app needs to tell readings and doses apart: an insulin dose's reason
|
|
634
|
+
/// (basal or bolus) and a glucose reading's meal time.
|
|
635
|
+
func getSamples(type: String, startDate: Double?, endDate: Double?, limit: Int?, completion rawCompletion: @escaping Completion) {
|
|
636
|
+
let completion = completeOnMain(rawCompletion)
|
|
637
|
+
guard let healthStore = healthStore else {
|
|
638
|
+
completion(.failure(CraftHealthError(message: "HealthKit not available")))
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
guard let unitName = Self.sampleUnits[type], let spec = Self.healthQuantity(type), let sampleType = spec.type else {
|
|
642
|
+
completion(.failure(CraftHealthError(message: "getSamples reads bloodGlucose, insulinDelivery or dietaryCarbohydrates", code: "INVALID_ARGUMENT")))
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
|
|
647
|
+
let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Calendar.current.date(byAdding: .day, value: -7, to: end)!
|
|
648
|
+
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
|
|
649
|
+
let oldestFirst = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
|
|
650
|
+
let cap = max(1, min(limit ?? 10_000, 50_000))
|
|
651
|
+
let ownBundle = Bundle.main.bundleIdentifier
|
|
652
|
+
|
|
653
|
+
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: cap, sortDescriptors: [oldestFirst]) { _, samples, error in
|
|
654
|
+
if let error = error, (error as? HKError)?.code != .errorNoData {
|
|
655
|
+
completion(.failure(CraftHealthError(message: error.localizedDescription)))
|
|
656
|
+
return
|
|
657
|
+
}
|
|
658
|
+
let rows: [[String: Any]] = (samples as? [HKQuantitySample] ?? []).map { sample in
|
|
659
|
+
let source = sample.sourceRevision.source
|
|
660
|
+
var item: [String: Any] = [
|
|
661
|
+
"id": sample.uuid.uuidString,
|
|
662
|
+
"value": sample.quantity.doubleValue(for: spec.unit),
|
|
663
|
+
"unit": unitName,
|
|
664
|
+
"startDate": sample.startDate.timeIntervalSince1970 * 1000,
|
|
665
|
+
"endDate": sample.endDate.timeIntervalSince1970 * 1000,
|
|
666
|
+
"sourceName": source.name,
|
|
667
|
+
"sourceBundleId": source.bundleIdentifier,
|
|
668
|
+
"ownApp": ownBundle != nil && source.bundleIdentifier == ownBundle,
|
|
669
|
+
]
|
|
670
|
+
if let device = sample.device?.name { item["deviceName"] = device }
|
|
671
|
+
if let reason = sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? NSNumber {
|
|
672
|
+
switch HKInsulinDeliveryReason(rawValue: reason.intValue) {
|
|
673
|
+
case .basal: item["insulinReason"] = "basal"
|
|
674
|
+
case .bolus: item["insulinReason"] = "bolus"
|
|
675
|
+
default: break
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if let meal = sample.metadata?[HKMetadataKeyBloodGlucoseMealTime] as? NSNumber {
|
|
679
|
+
switch HKBloodGlucoseMealTime(rawValue: meal.intValue) {
|
|
680
|
+
case .preprandial: item["mealTime"] = "beforeMeal"
|
|
681
|
+
case .postprandial: item["mealTime"] = "afterMeal"
|
|
682
|
+
default: break
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return item
|
|
686
|
+
}
|
|
687
|
+
completion(.success(rows))
|
|
688
|
+
}
|
|
689
|
+
healthStore.execute(query)
|
|
690
|
+
}
|
|
691
|
+
|
|
616
692
|
/// One heart-rate sample as a page sees it, from a read or the stream
|
|
617
693
|
/// alike: bpm, when, the app and device that wrote it, a series' count,
|
|
618
694
|
/// lowest and highest, and what the wearer was doing when the watch says.
|