@craft-native/ios 0.0.138 → 0.0.139
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 +6 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +140 -0
package/README.md
CHANGED
|
@@ -371,6 +371,12 @@ await health.requestAuthorization(['workouts', 'heartRate', 'sleep'], { write: f
|
|
|
371
371
|
const workouts = await health.getWorkouts({ startDate: Date.now() - 30 * 86400000 })
|
|
372
372
|
// One value per local day: sums, averages, the latest weight, hours asleep.
|
|
373
373
|
const hrv = await health.getDailyStatistics('heartRateVariability', { startDate: Date.now() - 30 * 86400000 })
|
|
374
|
+
// Every heart-rate reading in a range (bpm, start/end, source and device), oldest first.
|
|
375
|
+
const readings = await health.getHeartRateSamples({ startDate: Date.now() - 86400000 })
|
|
376
|
+
// The night as Apple Health recorded it: in bed, awake, and each asleep stage.
|
|
377
|
+
const nights = await health.getSleepIntervals({ startDate: Date.now() - 86400000 })
|
|
378
|
+
// An app built before these reads existed has neither; check first.
|
|
379
|
+
if (health.supports('getHeartRateSamples')) { /* ... */ }
|
|
374
380
|
|
|
375
381
|
// Screen Capture
|
|
376
382
|
const screenshotBase64 = await window.craft.takeScreenshot();
|
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -2358,6 +2358,27 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2358
2358
|
} else {
|
|
2359
2359
|
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2360
2360
|
}
|
|
2361
|
+
case "getHealthHeartRateSamples":
|
|
2362
|
+
if config.enableHealthKit {
|
|
2363
|
+
getHealthHeartRateSamples(
|
|
2364
|
+
startDate: body["startDate"] as? Double,
|
|
2365
|
+
endDate: body["endDate"] as? Double,
|
|
2366
|
+
limit: body["limit"] as? Int,
|
|
2367
|
+
callbackId: callbackId
|
|
2368
|
+
)
|
|
2369
|
+
} else {
|
|
2370
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2371
|
+
}
|
|
2372
|
+
case "getHealthSleepIntervals":
|
|
2373
|
+
if config.enableHealthKit {
|
|
2374
|
+
getHealthSleepIntervals(
|
|
2375
|
+
startDate: body["startDate"] as? Double,
|
|
2376
|
+
endDate: body["endDate"] as? Double,
|
|
2377
|
+
callbackId: callbackId
|
|
2378
|
+
)
|
|
2379
|
+
} else {
|
|
2380
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2381
|
+
}
|
|
2361
2382
|
|
|
2362
2383
|
// MARK: - Live Activities
|
|
2363
2384
|
case "startLiveActivity":
|
|
@@ -4854,6 +4875,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
4854
4875
|
getDailyStatistics: function(type, options) {
|
|
4855
4876
|
options = options || {};
|
|
4856
4877
|
return craft._invoke('getHealthDailyStatistics', {type: type, startDate: options.startDate, endDate: options.endDate});
|
|
4878
|
+
},
|
|
4879
|
+
getHeartRateSamples: function(options) {
|
|
4880
|
+
options = options || {};
|
|
4881
|
+
return craft._invoke('getHealthHeartRateSamples', {startDate: options.startDate, endDate: options.endDate, limit: options.limit});
|
|
4882
|
+
},
|
|
4883
|
+
getSleepIntervals: function(options) {
|
|
4884
|
+
options = options || {};
|
|
4885
|
+
return craft._invoke('getHealthSleepIntervals', {startDate: options.startDate, endDate: options.endDate});
|
|
4857
4886
|
}
|
|
4858
4887
|
};
|
|
4859
4888
|
craft.liveActivity = {
|
|
@@ -7260,6 +7289,117 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7260
7289
|
healthStore.execute(query)
|
|
7261
7290
|
}
|
|
7262
7291
|
|
|
7292
|
+
/// Every heart-rate reading between two instants, oldest first: the
|
|
7293
|
+
/// beats per minute, when it was taken, and the app and device that
|
|
7294
|
+
/// wrote it. A watch's workout readings can arrive as one series
|
|
7295
|
+
/// sample; those also carry their count, lowest and highest.
|
|
7296
|
+
private func getHealthHeartRateSamples(startDate: Double?, endDate: Double?, limit: Int?, callbackId: String?) {
|
|
7297
|
+
guard let healthStore = healthStore else {
|
|
7298
|
+
rejectCallback(callbackId, error: "HealthKit not available")
|
|
7299
|
+
return
|
|
7300
|
+
}
|
|
7301
|
+
guard let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
|
|
7302
|
+
rejectCallback(callbackId, error: "Heart rate is unavailable")
|
|
7303
|
+
return
|
|
7304
|
+
}
|
|
7305
|
+
|
|
7306
|
+
let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
|
|
7307
|
+
let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Calendar.current.date(byAdding: .day, value: -1, to: end)!
|
|
7308
|
+
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
|
|
7309
|
+
let oldestFirst = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
|
|
7310
|
+
let cap = max(1, min(limit ?? 10_000, 50_000))
|
|
7311
|
+
|
|
7312
|
+
let query = HKSampleQuery(sampleType: heartType, predicate: predicate, limit: cap, sortDescriptors: [oldestFirst]) { [weak self] _, samples, error in
|
|
7313
|
+
if let error = error, (error as? HKError)?.code != .errorNoData {
|
|
7314
|
+
self?.rejectCallback(callbackId, error: error.localizedDescription)
|
|
7315
|
+
return
|
|
7316
|
+
}
|
|
7317
|
+
|
|
7318
|
+
let bpm = HKUnit.count().unitDivided(by: .minute())
|
|
7319
|
+
let rows: [[String: Any]] = (samples as? [HKQuantitySample] ?? []).map { sample in
|
|
7320
|
+
var item: [String: Any] = [
|
|
7321
|
+
"bpm": sample.quantity.doubleValue(for: bpm),
|
|
7322
|
+
"startDate": sample.startDate.timeIntervalSince1970 * 1000,
|
|
7323
|
+
"endDate": sample.endDate.timeIntervalSince1970 * 1000,
|
|
7324
|
+
"sourceName": sample.sourceRevision.source.name,
|
|
7325
|
+
]
|
|
7326
|
+
if let device = sample.device?.name { item["deviceName"] = device }
|
|
7327
|
+
if let model = sample.device?.model { item["deviceModel"] = model }
|
|
7328
|
+
if let series = sample as? HKDiscreteQuantitySample, series.count > 1 {
|
|
7329
|
+
item["count"] = series.count
|
|
7330
|
+
item["min"] = series.minimumQuantity.doubleValue(for: bpm)
|
|
7331
|
+
item["max"] = series.maximumQuantity.doubleValue(for: bpm)
|
|
7332
|
+
}
|
|
7333
|
+
if let raw = sample.metadata?[HKMetadataKeyHeartRateMotionContext] as? NSNumber {
|
|
7334
|
+
switch HKHeartRateMotionContext(rawValue: raw.intValue) {
|
|
7335
|
+
case .sedentary: item["motionContext"] = "sedentary"
|
|
7336
|
+
case .active: item["motionContext"] = "active"
|
|
7337
|
+
default: break
|
|
7338
|
+
}
|
|
7339
|
+
}
|
|
7340
|
+
return item
|
|
7341
|
+
}
|
|
7342
|
+
self?.resolveCallback(callbackId, result: rows)
|
|
7343
|
+
}
|
|
7344
|
+
healthStore.execute(query)
|
|
7345
|
+
}
|
|
7346
|
+
|
|
7347
|
+
/// A stable name for each sleep analysis value: in bed, awake, or one
|
|
7348
|
+
/// of the asleep stages (`asleep` when the source does not stage it).
|
|
7349
|
+
private static func sleepStageName(_ value: Int) -> String {
|
|
7350
|
+
switch HKCategoryValueSleepAnalysis(rawValue: value) {
|
|
7351
|
+
case .inBed: return "inBed"
|
|
7352
|
+
case .awake: return "awake"
|
|
7353
|
+
case .asleepCore: return "core"
|
|
7354
|
+
case .asleepDeep: return "deep"
|
|
7355
|
+
case .asleepREM: return "rem"
|
|
7356
|
+
default: return "asleep"
|
|
7357
|
+
}
|
|
7358
|
+
}
|
|
7359
|
+
|
|
7360
|
+
/// Sleep between two instants, oldest first, as the sources wrote it:
|
|
7361
|
+
/// time in bed and every asleep or awake stretch, each with its stage
|
|
7362
|
+
/// and whether it counts as asleep, so a page can mark the night.
|
|
7363
|
+
private func getHealthSleepIntervals(startDate: Double?, endDate: Double?, callbackId: String?) {
|
|
7364
|
+
guard let healthStore = healthStore else {
|
|
7365
|
+
rejectCallback(callbackId, error: "HealthKit not available")
|
|
7366
|
+
return
|
|
7367
|
+
}
|
|
7368
|
+
guard let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else {
|
|
7369
|
+
rejectCallback(callbackId, error: "Sleep data is unavailable")
|
|
7370
|
+
return
|
|
7371
|
+
}
|
|
7372
|
+
|
|
7373
|
+
let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
|
|
7374
|
+
let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Calendar.current.date(byAdding: .day, value: -1, to: end)!
|
|
7375
|
+
// Any interval that overlaps the range, so a night that began
|
|
7376
|
+
// before it still counts.
|
|
7377
|
+
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
|
|
7378
|
+
let oldestFirst = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
|
|
7379
|
+
|
|
7380
|
+
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [oldestFirst]) { [weak self] _, samples, error in
|
|
7381
|
+
if let error = error, (error as? HKError)?.code != .errorNoData {
|
|
7382
|
+
self?.rejectCallback(callbackId, error: error.localizedDescription)
|
|
7383
|
+
return
|
|
7384
|
+
}
|
|
7385
|
+
|
|
7386
|
+
let notAsleep: Set<Int> = [HKCategoryValueSleepAnalysis.inBed.rawValue, HKCategoryValueSleepAnalysis.awake.rawValue]
|
|
7387
|
+
let rows: [[String: Any]] = (samples as? [HKCategorySample] ?? []).map { sample in
|
|
7388
|
+
var item: [String: Any] = [
|
|
7389
|
+
"startDate": sample.startDate.timeIntervalSince1970 * 1000,
|
|
7390
|
+
"endDate": sample.endDate.timeIntervalSince1970 * 1000,
|
|
7391
|
+
"stage": Self.sleepStageName(sample.value),
|
|
7392
|
+
"asleep": !notAsleep.contains(sample.value),
|
|
7393
|
+
"sourceName": sample.sourceRevision.source.name,
|
|
7394
|
+
]
|
|
7395
|
+
if let device = sample.device?.name { item["deviceName"] = device }
|
|
7396
|
+
return item
|
|
7397
|
+
}
|
|
7398
|
+
self?.resolveCallback(callbackId, result: rows)
|
|
7399
|
+
}
|
|
7400
|
+
healthStore.execute(query)
|
|
7401
|
+
}
|
|
7402
|
+
|
|
7263
7403
|
// MARK: - Live Activities
|
|
7264
7404
|
private func startLiveActivity(body: [String: Any], callbackId: String?) {
|
|
7265
7405
|
guard #available(iOS 16.2, *), ActivityAuthorizationInfo().areActivitiesEnabled else {
|