craft-native 0.0.139 → 0.0.141
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/dist/android/templates/CraftNative.kt.template +137 -0
- package/dist/android/templates/MainActivityNative.kt.template +822 -87
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +14 -482
- package/dist/ios/templates/CraftHealth.swift +551 -0
- package/dist/ios/templates/CraftNativeActions.swift +11 -0
- package/dist/ios/templates/CraftNativeScreen.swift +763 -54
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3303
3303
|
`);
|
|
3304
3304
|
}
|
|
3305
3305
|
// package.json
|
|
3306
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.141";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.js
CHANGED
|
@@ -778,6 +778,7 @@ async function init(options) {
|
|
|
778
778
|
cpSync(join(TEMPLATES_DIR, "CraftNativeScreen.swift"), join(output, "Sources", "CraftNativeScreen.swift"));
|
|
779
779
|
cpSync(join(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join(output, "Sources", "CraftNativeFlatList.swift"));
|
|
780
780
|
cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
|
|
781
|
+
cpSync(join(TEMPLATES_DIR, "CraftHealth.swift"), join(output, "Sources", "CraftHealth.swift"));
|
|
781
782
|
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
782
783
|
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
783
784
|
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
@@ -1596,7 +1596,6 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1596
1596
|
private var recordingURL: URL?
|
|
1597
1597
|
|
|
1598
1598
|
// Health
|
|
1599
|
-
private var healthStore: HKHealthStore?
|
|
1600
1599
|
|
|
1601
1600
|
// SQLite database
|
|
1602
1601
|
private var db: OpaquePointer?
|
|
@@ -1628,9 +1627,6 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1628
1627
|
if config.enableMotionSensors {
|
|
1629
1628
|
motionManager = CMMotionManager()
|
|
1630
1629
|
}
|
|
1631
|
-
if config.enableHealthKit && HKHealthStore.isHealthDataAvailable() {
|
|
1632
|
-
healthStore = HKHealthStore()
|
|
1633
|
-
}
|
|
1634
1630
|
if config.enableWatchApp {
|
|
1635
1631
|
setupWatchConnectivity()
|
|
1636
1632
|
}
|
|
@@ -6900,504 +6896,40 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
6900
6896
|
}
|
|
6901
6897
|
|
|
6902
6898
|
// MARK: - Health
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
var readTypes: Set<HKObjectType> = []
|
|
6910
|
-
var shareTypes: Set<HKSampleType> = [HKObjectType.workoutType()]
|
|
6911
|
-
if let routeType = HKSeriesType.workoutRoute() as? HKSampleType {
|
|
6912
|
-
shareTypes.insert(routeType)
|
|
6913
|
-
}
|
|
6914
|
-
|
|
6915
|
-
for type in types {
|
|
6916
|
-
switch type {
|
|
6917
|
-
case "steps":
|
|
6918
|
-
if let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) {
|
|
6919
|
-
readTypes.insert(stepType)
|
|
6920
|
-
}
|
|
6921
|
-
case "heartRate":
|
|
6922
|
-
if let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate) {
|
|
6923
|
-
readTypes.insert(heartType)
|
|
6924
|
-
}
|
|
6925
|
-
case "activeEnergy":
|
|
6926
|
-
if let energyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) {
|
|
6927
|
-
readTypes.insert(energyType)
|
|
6928
|
-
shareTypes.insert(energyType)
|
|
6929
|
-
}
|
|
6930
|
-
case "distance":
|
|
6931
|
-
if let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning) {
|
|
6932
|
-
readTypes.insert(distanceType)
|
|
6933
|
-
shareTypes.insert(distanceType)
|
|
6934
|
-
}
|
|
6935
|
-
case "workouts":
|
|
6936
|
-
readTypes.insert(HKObjectType.workoutType())
|
|
6937
|
-
// A workout's own heart rate and energy are read through
|
|
6938
|
-
// these; without them every workout comes back bare.
|
|
6939
|
-
if let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate) {
|
|
6940
|
-
readTypes.insert(heartType)
|
|
6941
|
-
}
|
|
6942
|
-
if let energyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) {
|
|
6943
|
-
readTypes.insert(energyType)
|
|
6944
|
-
}
|
|
6945
|
-
case "restingHeartRate":
|
|
6946
|
-
if let restingType = HKQuantityType.quantityType(forIdentifier: .restingHeartRate) {
|
|
6947
|
-
readTypes.insert(restingType)
|
|
6948
|
-
}
|
|
6949
|
-
case "heartRateVariability":
|
|
6950
|
-
if let hrvType = HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN) {
|
|
6951
|
-
readTypes.insert(hrvType)
|
|
6952
|
-
}
|
|
6953
|
-
case "bodyMass":
|
|
6954
|
-
if let massType = HKQuantityType.quantityType(forIdentifier: .bodyMass) {
|
|
6955
|
-
readTypes.insert(massType)
|
|
6956
|
-
}
|
|
6957
|
-
case "sleep":
|
|
6958
|
-
if let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) {
|
|
6959
|
-
readTypes.insert(sleepType)
|
|
6960
|
-
}
|
|
6961
|
-
default:
|
|
6962
|
-
break
|
|
6963
|
-
}
|
|
6899
|
+
// HealthKit itself is CraftHealth.swift, shared with native screens.
|
|
6900
|
+
private func answer(_ callbackId: String?, _ result: Result<Any, CraftHealthError>) {
|
|
6901
|
+
switch result {
|
|
6902
|
+
case .success(let value): resolveCallback(callbackId, result: value)
|
|
6903
|
+
case .failure(let failure): rejectCallback(callbackId, error: failure.message, code: failure.code)
|
|
6964
6904
|
}
|
|
6905
|
+
}
|
|
6965
6906
|
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
if readOnly { shareTypes.removeAll() }
|
|
6969
|
-
healthStore.requestAuthorization(toShare: shareTypes, read: readTypes) { [weak self] success, error in
|
|
6970
|
-
if success {
|
|
6971
|
-
self?.resolveCallback(callbackId, result: true)
|
|
6972
|
-
} else {
|
|
6973
|
-
self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Authorization failed")
|
|
6974
|
-
}
|
|
6975
|
-
}
|
|
6907
|
+
private func requestHealthAuthorization(types: [String], readOnly: Bool = false, callbackId: String?) {
|
|
6908
|
+
CraftHealth.shared.requestAuthorization(types: types, readOnly: readOnly) { [weak self] in self?.answer(callbackId, $0) }
|
|
6976
6909
|
}
|
|
6977
6910
|
|
|
6978
6911
|
private func getHealthData(type: String, startDate: Double?, endDate: Double?, callbackId: String?) {
|
|
6979
|
-
|
|
6980
|
-
rejectCallback(callbackId, error: "HealthKit not available")
|
|
6981
|
-
return
|
|
6982
|
-
}
|
|
6983
|
-
|
|
6984
|
-
guard let spec = Self.healthQuantity(type), let qType = spec.type else {
|
|
6985
|
-
rejectCallback(callbackId, error: "Unknown health data type")
|
|
6986
|
-
return
|
|
6987
|
-
}
|
|
6988
|
-
let qUnit = spec.unit
|
|
6989
|
-
|
|
6990
|
-
let start = startDate != nil ? Date(timeIntervalSince1970: startDate! / 1000) : Calendar.current.date(byAdding: .day, value: -7, to: Date())!
|
|
6991
|
-
let end = endDate != nil ? Date(timeIntervalSince1970: endDate! / 1000) : Date()
|
|
6992
|
-
|
|
6993
|
-
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
|
|
6994
|
-
|
|
6995
|
-
// A discrete type (heart rate, weight) has no sum: HealthKit fails
|
|
6996
|
-
// the whole query when asked for one, which is what heart rate
|
|
6997
|
-
// used to do. Each type is asked for the statistic it has.
|
|
6998
|
-
let query = HKStatisticsQuery(quantityType: qType, quantitySamplePredicate: predicate, options: spec.options) { [weak self] _, result, error in
|
|
6999
|
-
if let error = error {
|
|
7000
|
-
// No samples in the range is an answer, not a failure.
|
|
7001
|
-
if (error as? HKError)?.code == .errorNoData {
|
|
7002
|
-
self?.resolveCallback(callbackId, result: ["value": 0, "unit": qUnit.unitString])
|
|
7003
|
-
return
|
|
7004
|
-
}
|
|
7005
|
-
self?.rejectCallback(callbackId, error: error.localizedDescription)
|
|
7006
|
-
return
|
|
7007
|
-
}
|
|
7008
|
-
|
|
7009
|
-
let value = result.flatMap { Self.statisticValue($0, options: spec.options, unit: qUnit) } ?? 0
|
|
7010
|
-
self?.resolveCallback(callbackId, result: ["value": value, "unit": qUnit.unitString])
|
|
7011
|
-
}
|
|
7012
|
-
|
|
7013
|
-
healthStore.execute(query)
|
|
6912
|
+
CraftHealth.shared.getData(type: type, startDate: startDate, endDate: endDate) { [weak self] in self?.answer(callbackId, $0) }
|
|
7014
6913
|
}
|
|
7015
6914
|
|
|
7016
6915
|
private func saveHealthWorkout(body: [String: Any], callbackId: String?) {
|
|
7017
|
-
|
|
7018
|
-
rejectCallback(callbackId, error: "HealthKit not available")
|
|
7019
|
-
return
|
|
7020
|
-
}
|
|
7021
|
-
guard let activityId = body["activityId"] as? String,
|
|
7022
|
-
let type = body["type"] as? String,
|
|
7023
|
-
let startValue = body["startDate"] as? Double,
|
|
7024
|
-
let endValue = body["endDate"] as? Double,
|
|
7025
|
-
endValue > startValue else {
|
|
7026
|
-
rejectCallback(callbackId, error: "A valid activityId, type, startDate, and endDate are required")
|
|
7027
|
-
return
|
|
7028
|
-
}
|
|
7029
|
-
|
|
7030
|
-
let activityType: HKWorkoutActivityType
|
|
7031
|
-
switch type {
|
|
7032
|
-
case "running": activityType = .running
|
|
7033
|
-
case "walking": activityType = .walking
|
|
7034
|
-
case "hiking": activityType = .hiking
|
|
7035
|
-
case "cycling": activityType = .cycling
|
|
7036
|
-
default:
|
|
7037
|
-
rejectCallback(callbackId, error: "Unsupported workout type")
|
|
7038
|
-
return
|
|
7039
|
-
}
|
|
7040
|
-
|
|
7041
|
-
let distance = (body["distanceMeters"] as? Double).flatMap { value in
|
|
7042
|
-
value > 0 ? HKQuantity(unit: .meter(), doubleValue: value) : nil
|
|
7043
|
-
}
|
|
7044
|
-
let energy = (body["activeEnergyCalories"] as? Double).flatMap { value in
|
|
7045
|
-
value > 0 ? HKQuantity(unit: .kilocalorie(), doubleValue: value) : nil
|
|
7046
|
-
}
|
|
7047
|
-
let start = Date(timeIntervalSince1970: startValue / 1000)
|
|
7048
|
-
let end = Date(timeIntervalSince1970: endValue / 1000)
|
|
7049
|
-
let workout = HKWorkout(
|
|
7050
|
-
activityType: activityType,
|
|
7051
|
-
start: start,
|
|
7052
|
-
end: end,
|
|
7053
|
-
workoutEvents: nil,
|
|
7054
|
-
totalEnergyBurned: energy,
|
|
7055
|
-
totalDistance: distance,
|
|
7056
|
-
metadata: [HKMetadataKeyExternalUUID: activityId, HKMetadataKeyIndoorWorkout: false]
|
|
7057
|
-
)
|
|
7058
|
-
|
|
7059
|
-
healthStore.save(workout) { [weak self] success, error in
|
|
7060
|
-
guard let self = self else { return }
|
|
7061
|
-
guard success else {
|
|
7062
|
-
self.rejectCallback(callbackId, error: error?.localizedDescription ?? "Workout could not be saved")
|
|
7063
|
-
return
|
|
7064
|
-
}
|
|
7065
|
-
|
|
7066
|
-
let locations = (body["locations"] as? [[String: Any]] ?? []).compactMap { item -> CLLocation? in
|
|
7067
|
-
guard let latitude = item["latitude"] as? Double,
|
|
7068
|
-
let longitude = item["longitude"] as? Double,
|
|
7069
|
-
let timestamp = item["timestamp"] as? Double else { return nil }
|
|
7070
|
-
return CLLocation(
|
|
7071
|
-
coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude),
|
|
7072
|
-
altitude: item["altitude"] as? Double ?? 0,
|
|
7073
|
-
horizontalAccuracy: item["accuracy"] as? Double ?? 10,
|
|
7074
|
-
verticalAccuracy: -1,
|
|
7075
|
-
timestamp: Date(timeIntervalSince1970: timestamp / 1000)
|
|
7076
|
-
)
|
|
7077
|
-
}.filter { $0.timestamp >= start && $0.timestamp <= end }
|
|
7078
|
-
|
|
7079
|
-
guard !locations.isEmpty else {
|
|
7080
|
-
self.resolveCallback(callbackId, result: ["id": workout.uuid.uuidString])
|
|
7081
|
-
return
|
|
7082
|
-
}
|
|
7083
|
-
|
|
7084
|
-
let routeBuilder = HKWorkoutRouteBuilder(healthStore: healthStore, device: .local())
|
|
7085
|
-
routeBuilder.insertRouteData(locations) { inserted, routeError in
|
|
7086
|
-
guard inserted else {
|
|
7087
|
-
self.rejectCallback(callbackId, error: routeError?.localizedDescription ?? "Workout route could not be saved")
|
|
7088
|
-
return
|
|
7089
|
-
}
|
|
7090
|
-
routeBuilder.finishRoute(with: workout, metadata: [HKMetadataKeyExternalUUID: activityId]) { route, finishError in
|
|
7091
|
-
if let finishError = finishError {
|
|
7092
|
-
self.rejectCallback(callbackId, error: finishError.localizedDescription)
|
|
7093
|
-
} else {
|
|
7094
|
-
self.resolveCallback(callbackId, result: [
|
|
7095
|
-
"id": workout.uuid.uuidString,
|
|
7096
|
-
"routeId": route?.uuid.uuidString ?? ""
|
|
7097
|
-
])
|
|
7098
|
-
}
|
|
7099
|
-
}
|
|
7100
|
-
}
|
|
7101
|
-
}
|
|
7102
|
-
}
|
|
7103
|
-
|
|
7104
|
-
/// The quantity behind each health data type, its unit, and the one
|
|
7105
|
-
/// statistic that type supports.
|
|
7106
|
-
private static func healthQuantity(_ type: String) -> (type: HKQuantityType?, unit: HKUnit, options: HKStatisticsOptions)? {
|
|
7107
|
-
let bpm = HKUnit.count().unitDivided(by: .minute())
|
|
7108
|
-
switch type {
|
|
7109
|
-
case "steps": return (HKQuantityType.quantityType(forIdentifier: .stepCount), .count(), .cumulativeSum)
|
|
7110
|
-
case "activeEnergy": return (HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned), .kilocalorie(), .cumulativeSum)
|
|
7111
|
-
case "distance": return (HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning), .meter(), .cumulativeSum)
|
|
7112
|
-
case "heartRate": return (HKQuantityType.quantityType(forIdentifier: .heartRate), bpm, .discreteAverage)
|
|
7113
|
-
case "restingHeartRate": return (HKQuantityType.quantityType(forIdentifier: .restingHeartRate), bpm, .discreteAverage)
|
|
7114
|
-
case "heartRateVariability": return (HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN), .secondUnit(with: .milli), .discreteAverage)
|
|
7115
|
-
case "bodyMass": return (HKQuantityType.quantityType(forIdentifier: .bodyMass), .gramUnit(with: .kilo), .mostRecent)
|
|
7116
|
-
default: return nil
|
|
7117
|
-
}
|
|
7118
|
-
}
|
|
7119
|
-
|
|
7120
|
-
private static func statisticValue(_ statistics: HKStatistics, options: HKStatisticsOptions, unit: HKUnit) -> Double? {
|
|
7121
|
-
if options.contains(.cumulativeSum) { return statistics.sumQuantity()?.doubleValue(for: unit) }
|
|
7122
|
-
if options.contains(.mostRecent) { return statistics.mostRecentQuantity()?.doubleValue(for: unit) }
|
|
7123
|
-
return statistics.averageQuantity()?.doubleValue(for: unit)
|
|
6916
|
+
CraftHealth.shared.saveWorkout(body: body) { [weak self] in self?.answer(callbackId, $0) }
|
|
7124
6917
|
}
|
|
7125
6918
|
|
|
7126
|
-
private static let healthDayFormatter: DateFormatter = {
|
|
7127
|
-
let formatter = DateFormatter()
|
|
7128
|
-
formatter.calendar = Calendar(identifier: .gregorian)
|
|
7129
|
-
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
7130
|
-
formatter.timeZone = .current
|
|
7131
|
-
formatter.dateFormat = "yyyy-MM-dd"
|
|
7132
|
-
return formatter
|
|
7133
|
-
}()
|
|
7134
|
-
|
|
7135
|
-
/// A stable name for a workout's activity, independent of the SDK's
|
|
7136
|
-
/// raw values, so a page can map it to its own sports.
|
|
7137
|
-
private static func workoutTypeName(_ type: HKWorkoutActivityType) -> String {
|
|
7138
|
-
switch type {
|
|
7139
|
-
case .running: return "running"
|
|
7140
|
-
case .cycling: return "cycling"
|
|
7141
|
-
case .walking: return "walking"
|
|
7142
|
-
case .hiking: return "hiking"
|
|
7143
|
-
case .swimming: return "swimming"
|
|
7144
|
-
case .rowing: return "rowing"
|
|
7145
|
-
case .elliptical: return "elliptical"
|
|
7146
|
-
case .stairClimbing, .stairs: return "stairClimbing"
|
|
7147
|
-
case .yoga: return "yoga"
|
|
7148
|
-
case .pilates: return "pilates"
|
|
7149
|
-
case .functionalStrengthTraining, .traditionalStrengthTraining, .coreTraining: return "strength"
|
|
7150
|
-
case .highIntensityIntervalTraining: return "hiit"
|
|
7151
|
-
case .crossTraining, .mixedCardio: return "crossTraining"
|
|
7152
|
-
case .crossCountrySkiing: return "crossCountrySkiing"
|
|
7153
|
-
case .downhillSkiing, .snowboarding: return "skiing"
|
|
7154
|
-
case .paddleSports: return "paddling"
|
|
7155
|
-
case .climbing: return "climbing"
|
|
7156
|
-
case .dance, .socialDance, .cardioDance: return "dance"
|
|
7157
|
-
case .cooldown, .flexibility, .mindAndBody: return "mobility"
|
|
7158
|
-
default: return "other"
|
|
7159
|
-
}
|
|
7160
|
-
}
|
|
7161
|
-
|
|
7162
|
-
/// Workouts in Apple Health — the watch's, and every app's that
|
|
7163
|
-
/// writes there — newest first, with the numbers a training log needs.
|
|
7164
6919
|
private func getHealthWorkouts(startDate: Double?, endDate: Double?, limit: Int?, callbackId: String?) {
|
|
7165
|
-
|
|
7166
|
-
rejectCallback(callbackId, error: "HealthKit not available")
|
|
7167
|
-
return
|
|
7168
|
-
}
|
|
7169
|
-
|
|
7170
|
-
let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Calendar.current.date(byAdding: .day, value: -30, to: Date())!
|
|
7171
|
-
let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
|
|
7172
|
-
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
|
|
7173
|
-
let newestFirst = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
|
|
7174
|
-
let cap = max(1, min(limit ?? 200, 1000))
|
|
7175
|
-
|
|
7176
|
-
let query = HKSampleQuery(sampleType: HKObjectType.workoutType(), predicate: predicate, limit: cap, sortDescriptors: [newestFirst]) { [weak self] _, samples, error in
|
|
7177
|
-
if let error = error, (error as? HKError)?.code != .errorNoData {
|
|
7178
|
-
self?.rejectCallback(callbackId, error: error.localizedDescription)
|
|
7179
|
-
return
|
|
7180
|
-
}
|
|
7181
|
-
|
|
7182
|
-
let bpm = HKUnit.count().unitDivided(by: .minute())
|
|
7183
|
-
let workouts: [[String: Any]] = (samples as? [HKWorkout] ?? []).map { workout in
|
|
7184
|
-
var item: [String: Any] = [
|
|
7185
|
-
"id": workout.uuid.uuidString,
|
|
7186
|
-
"type": Self.workoutTypeName(workout.workoutActivityType),
|
|
7187
|
-
"startDate": workout.startDate.timeIntervalSince1970 * 1000,
|
|
7188
|
-
"endDate": workout.endDate.timeIntervalSince1970 * 1000,
|
|
7189
|
-
"durationSeconds": workout.duration,
|
|
7190
|
-
"sourceName": workout.sourceRevision.source.name,
|
|
7191
|
-
"indoor": (workout.metadata?[HKMetadataKeyIndoorWorkout] as? Bool) ?? false,
|
|
7192
|
-
]
|
|
7193
|
-
if let meters = workout.totalDistance?.doubleValue(for: .meter()), meters > 0 {
|
|
7194
|
-
item["distanceMeters"] = meters
|
|
7195
|
-
}
|
|
7196
|
-
if let ascent = workout.metadata?[HKMetadataKeyElevationAscended] as? HKQuantity {
|
|
7197
|
-
item["elevationGainMeters"] = ascent.doubleValue(for: .meter())
|
|
7198
|
-
}
|
|
7199
|
-
if #available(iOS 16.0, *) {
|
|
7200
|
-
if let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate),
|
|
7201
|
-
let heart = workout.statistics(for: heartType) {
|
|
7202
|
-
if let average = heart.averageQuantity()?.doubleValue(for: bpm) { item["averageHeartRate"] = average }
|
|
7203
|
-
if let maximum = heart.maximumQuantity()?.doubleValue(for: bpm) { item["maxHeartRate"] = maximum }
|
|
7204
|
-
}
|
|
7205
|
-
if let energyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned),
|
|
7206
|
-
let calories = workout.statistics(for: energyType)?.sumQuantity()?.doubleValue(for: .kilocalorie()) {
|
|
7207
|
-
item["activeEnergyCalories"] = calories
|
|
7208
|
-
}
|
|
7209
|
-
} else if let calories = workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) {
|
|
7210
|
-
item["activeEnergyCalories"] = calories
|
|
7211
|
-
}
|
|
7212
|
-
return item
|
|
7213
|
-
}
|
|
7214
|
-
self?.resolveCallback(callbackId, result: workouts)
|
|
7215
|
-
}
|
|
7216
|
-
healthStore.execute(query)
|
|
6920
|
+
CraftHealth.shared.getWorkouts(startDate: startDate, endDate: endDate, limit: limit) { [weak self] in self?.answer(callbackId, $0) }
|
|
7217
6921
|
}
|
|
7218
6922
|
|
|
7219
|
-
/// One value per local day: a sum for steps, energy and distance, an
|
|
7220
|
-
/// average for heart rate, resting heart rate and HRV, the latest
|
|
7221
|
-
/// weight, and hours asleep for sleep (counted on the day you woke).
|
|
7222
6923
|
private func getHealthDailyStatistics(type: String, startDate: Double?, endDate: Double?, callbackId: String?) {
|
|
7223
|
-
|
|
7224
|
-
rejectCallback(callbackId, error: "HealthKit not available")
|
|
7225
|
-
return
|
|
7226
|
-
}
|
|
7227
|
-
|
|
7228
|
-
let calendar = Calendar.current
|
|
7229
|
-
let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
|
|
7230
|
-
let start = calendar.startOfDay(for: startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? calendar.date(byAdding: .day, value: -30, to: end)!)
|
|
7231
|
-
|
|
7232
|
-
if type == "sleep" {
|
|
7233
|
-
guard let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else {
|
|
7234
|
-
rejectCallback(callbackId, error: "Sleep data is unavailable")
|
|
7235
|
-
return
|
|
7236
|
-
}
|
|
7237
|
-
// From the evening before the first day, so its night counts.
|
|
7238
|
-
let predicate = HKQuery.predicateForSamples(withStart: calendar.date(byAdding: .hour, value: -12, to: start), end: end, options: [])
|
|
7239
|
-
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { [weak self] _, samples, error in
|
|
7240
|
-
if let error = error, (error as? HKError)?.code != .errorNoData {
|
|
7241
|
-
self?.rejectCallback(callbackId, error: error.localizedDescription)
|
|
7242
|
-
return
|
|
7243
|
-
}
|
|
7244
|
-
// Time in bed and time awake are not sleep; every asleep
|
|
7245
|
-
// stage (and the older single "asleep" value) is.
|
|
7246
|
-
let awake: Set<Int> = [HKCategoryValueSleepAnalysis.inBed.rawValue, HKCategoryValueSleepAnalysis.awake.rawValue]
|
|
7247
|
-
var secondsByDay: [String: Double] = [:]
|
|
7248
|
-
for sample in samples as? [HKCategorySample] ?? [] where !awake.contains(sample.value) {
|
|
7249
|
-
let day = Self.healthDayFormatter.string(from: sample.endDate)
|
|
7250
|
-
secondsByDay[day, default: 0] += sample.endDate.timeIntervalSince(sample.startDate)
|
|
7251
|
-
}
|
|
7252
|
-
let days = secondsByDay.keys.sorted().map { day -> [String: Any] in
|
|
7253
|
-
["date": day, "value": (secondsByDay[day]! / 3600 * 100).rounded() / 100, "unit": "hr"]
|
|
7254
|
-
}
|
|
7255
|
-
self?.resolveCallback(callbackId, result: days)
|
|
7256
|
-
}
|
|
7257
|
-
healthStore.execute(query)
|
|
7258
|
-
return
|
|
7259
|
-
}
|
|
7260
|
-
|
|
7261
|
-
guard let spec = Self.healthQuantity(type), let qType = spec.type else {
|
|
7262
|
-
rejectCallback(callbackId, error: "Unknown health data type")
|
|
7263
|
-
return
|
|
7264
|
-
}
|
|
7265
|
-
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
|
|
7266
|
-
let query = HKStatisticsCollectionQuery(
|
|
7267
|
-
quantityType: qType,
|
|
7268
|
-
quantitySamplePredicate: predicate,
|
|
7269
|
-
options: spec.options,
|
|
7270
|
-
anchorDate: start,
|
|
7271
|
-
intervalComponents: DateComponents(day: 1)
|
|
7272
|
-
)
|
|
7273
|
-
query.initialResultsHandler = { [weak self] _, collection, error in
|
|
7274
|
-
if let error = error, (error as? HKError)?.code != .errorNoData {
|
|
7275
|
-
self?.rejectCallback(callbackId, error: error.localizedDescription)
|
|
7276
|
-
return
|
|
7277
|
-
}
|
|
7278
|
-
var days: [[String: Any]] = []
|
|
7279
|
-
collection?.enumerateStatistics(from: start, to: end) { statistics, _ in
|
|
7280
|
-
guard let value = Self.statisticValue(statistics, options: spec.options, unit: spec.unit) else { return }
|
|
7281
|
-
days.append([
|
|
7282
|
-
"date": Self.healthDayFormatter.string(from: statistics.startDate),
|
|
7283
|
-
"value": value,
|
|
7284
|
-
"unit": spec.unit.unitString,
|
|
7285
|
-
])
|
|
7286
|
-
}
|
|
7287
|
-
self?.resolveCallback(callbackId, result: days)
|
|
7288
|
-
}
|
|
7289
|
-
healthStore.execute(query)
|
|
6924
|
+
CraftHealth.shared.getDailyStatistics(type: type, startDate: startDate, endDate: endDate) { [weak self] in self?.answer(callbackId, $0) }
|
|
7290
6925
|
}
|
|
7291
6926
|
|
|
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
6927
|
private func getHealthHeartRateSamples(startDate: Double?, endDate: Double?, limit: Int?, callbackId: String?) {
|
|
7297
|
-
|
|
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
|
-
}
|
|
6928
|
+
CraftHealth.shared.getHeartRateSamples(startDate: startDate, endDate: endDate, limit: limit) { [weak self] in self?.answer(callbackId, $0) }
|
|
7358
6929
|
}
|
|
7359
6930
|
|
|
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
6931
|
private func getHealthSleepIntervals(startDate: Double?, endDate: Double?, callbackId: String?) {
|
|
7364
|
-
|
|
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)
|
|
6932
|
+
CraftHealth.shared.getSleepIntervals(startDate: startDate, endDate: endDate) { [weak self] in self?.answer(callbackId, $0) }
|
|
7401
6933
|
}
|
|
7402
6934
|
|
|
7403
6935
|
// MARK: - Live Activities
|