@kingstinct/react-native-healthkit 14.1.0 → 15.1.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.
- package/README.md +32 -2
- package/app.plugin.js +49 -94
- package/app.plugin.ts +6 -56
- package/ios/BackgroundDeliveryManager.swift +13 -6
- package/ios/BackgroundLaunchHook.mm +41 -0
- package/ios/Constants.swift +24 -0
- package/ios/CoreModule.swift +5 -1
- package/ios/Helpers.swift +29 -1
- package/ios/MemoryEstimation.swift +83 -0
- package/ios/QuantityTypeModule.swift +34 -26
- package/ios/Serializers.swift +18 -3
- package/ios/SourceProxy.swift +14 -5
- package/ios/WorkoutProxy.swift +28 -11
- package/lib/commonjs/generated/healthkit.generated.js +13 -3
- package/lib/commonjs/healthkit.ios.js +3 -1
- package/lib/commonjs/healthkit.js +4 -2
- package/lib/module/generated/healthkit.generated.js +12 -2
- package/lib/module/healthkit.ios.js +2 -0
- package/lib/module/healthkit.js +2 -0
- package/lib/typescript/generated/healthkit.generated.d.ts +17 -7
- package/lib/typescript/healthkit.d.ts +1 -0
- package/lib/typescript/healthkit.ios.d.ts +2 -0
- package/lib/typescript/specs/CoreModule.nitro.d.ts +6 -4
- package/lib/typescript/specs/SourceProxy.nitro.d.ts +1 -2
- package/lib/typescript/types/QuantityType.d.ts +3 -3
- package/lib/typescript/types/Source.d.ts +2 -2
- package/lib/typescript/types/Units.d.ts +1 -1
- package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.hpp +23 -12
- package/nitrogen/generated/ios/c++/HybridCategoryTypeModuleSpecSwift.hpp +7 -4
- package/nitrogen/generated/ios/c++/HybridCorrelationTypeModuleSpecSwift.hpp +7 -4
- package/nitrogen/generated/ios/c++/HybridElectrocardiogramModuleSpecSwift.hpp +7 -4
- package/nitrogen/generated/ios/c++/HybridHeartbeatSeriesModuleSpecSwift.hpp +7 -4
- package/nitrogen/generated/ios/c++/HybridMedicationModuleSpecSwift.hpp +7 -4
- package/nitrogen/generated/ios/c++/HybridQuantityTypeModuleSpecSwift.hpp +7 -4
- package/nitrogen/generated/ios/c++/HybridStateOfMindModuleSpecSwift.hpp +7 -4
- package/nitrogen/generated/ios/c++/HybridWorkoutProxySpecSwift.hpp +3 -4
- package/nitrogen/generated/ios/c++/HybridWorkoutsModuleSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/CategoryTypeIdentifier.swift +8 -0
- package/nitrogen/generated/ios/swift/CategoryTypeIdentifierWriteable.swift +8 -0
- package/nitrogen/generated/ios/swift/ObjectTypeIdentifier.swift +8 -4
- package/nitrogen/generated/ios/swift/QuantityTypeIdentifier.swift +0 -4
- package/nitrogen/generated/ios/swift/QuantityTypeIdentifierWriteable.swift +0 -4
- package/nitrogen/generated/ios/swift/QueryStatisticsResponse.swift +6 -13
- package/nitrogen/generated/ios/swift/QueryStatisticsResponseFromSingleSource.swift +4 -11
- package/nitrogen/generated/ios/swift/SampleTypeIdentifier.swift +8 -4
- package/nitrogen/generated/ios/swift/SampleTypeIdentifierWriteable.swift +8 -4
- package/nitrogen/generated/ios/swift/SourceRevision.swift +4 -11
- package/nitrogen/generated/ios/swift/WorkoutActivityType.swift +8 -0
- package/nitrogen/generated/shared/c++/CategoryTypeIdentifier.hpp +67 -59
- package/nitrogen/generated/shared/c++/CategoryTypeIdentifierWriteable.hpp +67 -59
- package/nitrogen/generated/shared/c++/ObjectTypeIdentifier.hpp +202 -198
- package/nitrogen/generated/shared/c++/QuantityTypeIdentifier.hpp +107 -111
- package/nitrogen/generated/shared/c++/QuantityTypeIdentifierWriteable.hpp +107 -111
- package/nitrogen/generated/shared/c++/QueryStatisticsResponse.hpp +8 -9
- package/nitrogen/generated/shared/c++/QueryStatisticsResponseFromSingleSource.hpp +8 -9
- package/nitrogen/generated/shared/c++/SampleTypeIdentifier.hpp +195 -191
- package/nitrogen/generated/shared/c++/SampleTypeIdentifierWriteable.hpp +189 -185
- package/nitrogen/generated/shared/c++/SourceRevision.hpp +8 -9
- package/nitrogen/generated/shared/c++/WorkoutActivityType.hpp +4 -0
- package/package.json +5 -3
- package/src/generated/healthkit-schema.json +45 -8
- package/src/generated/healthkit.generated.ts +18 -6
- package/src/healthkit.ios.ts +3 -0
- package/src/healthkit.ts +5 -0
- package/src/specs/CoreModule.nitro.ts +6 -4
- package/src/specs/SourceProxy.nitro.ts +1 -1
- package/src/type-tests/generated-typing.ts +30 -8
- package/src/types/QuantityType.ts +3 -3
- package/src/types/Source.ts +2 -2
- package/src/types/Units.ts +1 -1
package/README.md
CHANGED
|
@@ -121,13 +121,13 @@ Some imperative examples:
|
|
|
121
121
|
'HKQuantityTypeIdentifierInsulinDelivery',
|
|
122
122
|
'IU',
|
|
123
123
|
5.5,
|
|
124
|
+
new Date(),
|
|
125
|
+
new Date(),
|
|
124
126
|
{
|
|
125
|
-
metadata: {
|
|
126
127
|
// Metadata keys could be arbirtary string to store app-specific data.
|
|
127
128
|
// To use built-in types from https://developer.apple.com/documentation/healthkit/samples/metadata_keys
|
|
128
129
|
// you need to specify string values instead of variable names (by dropping MetadataKey from the name).
|
|
129
130
|
HKInsulinDeliveryReason: HKInsulinDeliveryReason.basal,
|
|
130
|
-
},
|
|
131
131
|
}
|
|
132
132
|
);
|
|
133
133
|
```
|
|
@@ -154,6 +154,36 @@ Example:
|
|
|
154
154
|
// etc..
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
### Memory considerations for workouts and other native objects
|
|
158
|
+
|
|
159
|
+
Workouts (and a few other results, like sources) are returned as *proxies*: native objects that keep the underlying `HKWorkout` alive so you can call follow-up functions such as `getWorkoutRoutes()`, `getStatistic()` or `getAllStatistics()` on them. The JavaScript garbage collector frees a proxy, and the HealthKit objects behind it, once it is no longer referenced. The library reports an estimate of each proxy's native size to the JS engine so it can schedule collections sensibly. Note that react-native-nitro-modules currently keeps a small fixed amount of bookkeeping (on the order of a hundred bytes) per native object it has handed to JS for the lifetime of the JS runtime, so memory will not return exactly to baseline even after collection. If you process large numbers of workouts in a loop you can release the HealthKit side eagerly by calling `dispose()` once you are done with a workout:
|
|
160
|
+
|
|
161
|
+
```TypeScript
|
|
162
|
+
const workouts = await queryWorkoutSamples({ limit: 0 })
|
|
163
|
+
|
|
164
|
+
for (const workout of workouts) {
|
|
165
|
+
const routes = await workout.getWorkoutRoutes()
|
|
166
|
+
// ... do something with the routes
|
|
167
|
+
workout.dispose()
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
After `dispose()` the proxy can no longer be used - calling any function or reading any property on it will throw. If you need to keep the data around, call `toJSON()` first to get a plain object and store that instead of the proxy:
|
|
172
|
+
|
|
173
|
+
```TypeScript
|
|
174
|
+
const plainWorkouts = workouts.map((workout) => {
|
|
175
|
+
const json = workout.toJSON()
|
|
176
|
+
workout.dispose()
|
|
177
|
+
return json
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Other tips for keeping memory in check when working with large datasets:
|
|
182
|
+
- Query in chunks (for example per month, or with `limit` and anchors) instead of fetching everything at once.
|
|
183
|
+
- Prefer statistics queries (`queryStatisticsForQuantity`, `queryStatisticsCollectionForQuantity`) over fetching every sample when you only need aggregates.
|
|
184
|
+
- Keep plain data (from `toJSON()` or the sample queries) in React state rather than proxies.
|
|
185
|
+
- `sourceRevision.source` on samples and `sources` on statistics are plain objects, not proxies. To filter queries by a source, get a `SourceProxy` from `querySources()` and match it by `bundleIdentifier`.
|
|
186
|
+
|
|
157
187
|
## Migration to 9.0.0
|
|
158
188
|
|
|
159
189
|
There are a lot of under-the-hood changes in version 9.0.0, some of them are breaking (although I've tried to reduce it as much as possible).
|
package/app.plugin.js
CHANGED
|
@@ -1,94 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @type {ConfigPlugin<InfoPlistConfig>}
|
|
52
|
-
*/
|
|
53
|
-
const withInfoPlistPlugin = (
|
|
54
|
-
config,
|
|
55
|
-
/**
|
|
56
|
-
* @type {{NSHealthShareUsageDescription?: string | true, NSHealthUpdateUsageDescription?: string | false} | undefined}
|
|
57
|
-
* */
|
|
58
|
-
props,
|
|
59
|
-
) =>
|
|
60
|
-
withInfoPlist(config, (config) => {
|
|
61
|
-
config.modResults.NSHealthShareUsageDescription =
|
|
62
|
-
typeof props?.NSHealthShareUsageDescription === 'string'
|
|
63
|
-
? props.NSHealthShareUsageDescription
|
|
64
|
-
: typeof config.modResults.NSHealthShareUsageDescription === 'string'
|
|
65
|
-
? config.modResults.NSHealthShareUsageDescription
|
|
66
|
-
: `${config.name} wants to read your health data`
|
|
67
|
-
|
|
68
|
-
if (props?.NSHealthUpdateUsageDescription !== false) {
|
|
69
|
-
config.modResults.NSHealthUpdateUsageDescription =
|
|
70
|
-
typeof props?.NSHealthUpdateUsageDescription === 'string'
|
|
71
|
-
? props.NSHealthUpdateUsageDescription
|
|
72
|
-
: typeof config.modResults.NSHealthUpdateUsageDescription === 'string'
|
|
73
|
-
? config.modResults.NSHealthUpdateUsageDescription
|
|
74
|
-
: `${config.name} wants to update your health data`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return config
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const pkg = require('./package.json')
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @type {ConfigPlugin<AppPluginConfig>}
|
|
84
|
-
*/
|
|
85
|
-
const healthkitAppPlugin = (config, props) =>
|
|
86
|
-
withPlugins(config, [
|
|
87
|
-
[withEntitlementsPlugin, props],
|
|
88
|
-
[withInfoPlistPlugin, props],
|
|
89
|
-
])
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @type {ConfigPlugin<AppPluginConfig>}
|
|
93
|
-
*/
|
|
94
|
-
module.exports = createRunOncePlugin(healthkitAppPlugin, pkg.name, pkg.version)
|
|
1
|
+
// GENERATED FILE - do not edit by hand (gitignored).
|
|
2
|
+
// Compiled from app.plugin.ts by `bun run build:plugin`.
|
|
3
|
+
// Expo loads this file, not the .ts source, so edit app.plugin.ts and rebuild.
|
|
4
|
+
"use strict";
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
10
|
+
const package_json_1 = __importDefault(require("./package.json"));
|
|
11
|
+
const withEntitlementsPlugin = (config, props) => {
|
|
12
|
+
return (0, config_plugins_1.withEntitlementsPlist)(config, (configPlist) => {
|
|
13
|
+
configPlist.modResults['com.apple.developer.healthkit'] = true;
|
|
14
|
+
// background is enabled by default, but possible to opt-out from
|
|
15
|
+
// (haven't seen any drawbacks from having it enabled)
|
|
16
|
+
if (props?.background !== false) {
|
|
17
|
+
configPlist.modResults['com.apple.developer.healthkit.background-delivery'] = true;
|
|
18
|
+
}
|
|
19
|
+
return configPlist;
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const withInfoPlistPlugin = (config, props) => {
|
|
23
|
+
return (0, config_plugins_1.withInfoPlist)(config, (configPlist) => {
|
|
24
|
+
const existingShareDescription = configPlist.modResults.NSHealthShareUsageDescription;
|
|
25
|
+
configPlist.modResults.NSHealthShareUsageDescription =
|
|
26
|
+
typeof props?.NSHealthShareUsageDescription === 'string'
|
|
27
|
+
? props.NSHealthShareUsageDescription
|
|
28
|
+
: typeof existingShareDescription === 'string'
|
|
29
|
+
? existingShareDescription
|
|
30
|
+
: `${config.name ?? package_json_1.default.name} wants to read your health data`;
|
|
31
|
+
if (props?.NSHealthUpdateUsageDescription !== false) {
|
|
32
|
+
const existingUpdateDescription = configPlist.modResults.NSHealthUpdateUsageDescription;
|
|
33
|
+
configPlist.modResults.NSHealthUpdateUsageDescription =
|
|
34
|
+
typeof props?.NSHealthUpdateUsageDescription === 'string'
|
|
35
|
+
? props.NSHealthUpdateUsageDescription
|
|
36
|
+
: typeof existingUpdateDescription === 'string'
|
|
37
|
+
? existingUpdateDescription
|
|
38
|
+
: `${config.name ?? package_json_1.default.name} wants to update your health data`;
|
|
39
|
+
}
|
|
40
|
+
return configPlist;
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
const healthkitAppPlugin = (config, props) => {
|
|
44
|
+
return (0, config_plugins_1.withPlugins)(config, [
|
|
45
|
+
[withEntitlementsPlugin, props],
|
|
46
|
+
[withInfoPlistPlugin, props],
|
|
47
|
+
]);
|
|
48
|
+
};
|
|
49
|
+
exports.default = (0, config_plugins_1.createRunOncePlugin)(healthkitAppPlugin, package_json_1.default.name, package_json_1.default.version);
|
package/app.plugin.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ConfigPlugin,
|
|
3
3
|
createRunOncePlugin,
|
|
4
|
-
withAppDelegate,
|
|
5
4
|
withEntitlementsPlist,
|
|
6
5
|
withInfoPlist,
|
|
7
6
|
withPlugins,
|
|
@@ -17,6 +16,12 @@ type InfoPlistConfig = {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
type AppPluginConfig = InfoPlistConfig & {
|
|
19
|
+
/**
|
|
20
|
+
* Adds the background-delivery entitlement. Enabled by default; set to
|
|
21
|
+
* `false` to opt out. Launch-time observer registration needs no AppDelegate
|
|
22
|
+
* change: the pod hooks UIApplicationDidFinishLaunchingNotification itself
|
|
23
|
+
* (see ios/BackgroundLaunchHook.mm).
|
|
24
|
+
*/
|
|
20
25
|
background?: BackgroundConfig
|
|
21
26
|
}
|
|
22
27
|
|
|
@@ -64,65 +69,10 @@ const withInfoPlistPlugin: ConfigPlugin<InfoPlistConfig> = (config, props) => {
|
|
|
64
69
|
})
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
const withAppDelegatePlugin: ConfigPlugin<{
|
|
68
|
-
background?: BackgroundConfig
|
|
69
|
-
}> = (config, props) => {
|
|
70
|
-
if (props?.background === false) {
|
|
71
|
-
return config
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return withAppDelegate(config, (configDelegate) => {
|
|
75
|
-
const contents = configDelegate.modResults.contents
|
|
76
|
-
|
|
77
|
-
// Add import for HealthKit if not already present
|
|
78
|
-
if (!contents.includes('import HealthKit')) {
|
|
79
|
-
configDelegate.modResults.contents =
|
|
80
|
-
configDelegate.modResults.contents.replace(
|
|
81
|
-
/^(import .+\n)/m,
|
|
82
|
-
'$1import HealthKit\n',
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Insert BackgroundDeliveryManager setup into didFinishLaunchingWithOptions
|
|
87
|
-
const setupCall =
|
|
88
|
-
' BackgroundDeliveryManager.shared.setupBackgroundObservers()\n'
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
!configDelegate.modResults.contents.includes('BackgroundDeliveryManager')
|
|
92
|
-
) {
|
|
93
|
-
// Match the opening of didFinishLaunchingWithOptions and insert after the
|
|
94
|
-
// opening brace. `[^{]*` rather than `.+` for two reasons: `.` does not
|
|
95
|
-
// match newlines, and Expo SDK 54+ templates spread this signature over
|
|
96
|
-
// several lines; and refusing to cross a `{` keeps the match from
|
|
97
|
-
// spanning out of an earlier `application(...)` overload into this one.
|
|
98
|
-
const withSetup = configDelegate.modResults.contents.replace(
|
|
99
|
-
/(func application\([^{]*didFinishLaunchingWithOptions[^{]*\{)\n/,
|
|
100
|
-
`$1\n${setupCall}`,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
if (withSetup === configDelegate.modResults.contents) {
|
|
104
|
-
// Don't fail the build, but don't fail silently either — background
|
|
105
|
-
// delivery just won't survive app termination, which is otherwise
|
|
106
|
-
// very hard to trace back to here.
|
|
107
|
-
console.warn(
|
|
108
|
-
'[react-native-healthkit] Could not find didFinishLaunchingWithOptions in AppDelegate; ' +
|
|
109
|
-
'BackgroundDeliveryManager.shared.setupBackgroundObservers() was not inserted. ' +
|
|
110
|
-
'HealthKit background delivery will not work until it is added manually.',
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
configDelegate.modResults.contents = withSetup
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return configDelegate
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
|
|
121
72
|
const healthkitAppPlugin: ConfigPlugin<AppPluginConfig> = (config, props) => {
|
|
122
73
|
return withPlugins(config, [
|
|
123
74
|
[withEntitlementsPlugin, props],
|
|
124
75
|
[withInfoPlistPlugin, props],
|
|
125
|
-
[withAppDelegatePlugin, props],
|
|
126
76
|
])
|
|
127
77
|
}
|
|
128
78
|
|
|
@@ -9,13 +9,15 @@ private let bgLog = OSLog(subsystem: "com.kingstinct.healthkit", category: "Back
|
|
|
9
9
|
/// be set up in `application(_:didFinishLaunchingWithOptions:)` to receive background
|
|
10
10
|
/// delivery callbacks after the app has been terminated.
|
|
11
11
|
///
|
|
12
|
-
///
|
|
13
|
-
///
|
|
12
|
+
/// `BackgroundLaunchHook.mm` calls `setupBackgroundObservers()` on
|
|
13
|
+
/// UIApplicationDidFinishLaunchingNotification, so no AppDelegate change is needed.
|
|
14
|
+
/// The `@objc(BackgroundDeliveryManager)` name is what that hook looks up at runtime.
|
|
14
15
|
///
|
|
15
16
|
/// The types to observe are persisted in UserDefaults by `configureBackgroundTypes()`
|
|
16
17
|
/// called from JS. On subsequent cold launches, the manager reads these and registers
|
|
17
18
|
/// observers immediately, queuing any events until JS subscribes via `drainPendingEvents()`.
|
|
18
|
-
@objc
|
|
19
|
+
@objc(BackgroundDeliveryManager)
|
|
20
|
+
public class BackgroundDeliveryManager: NSObject {
|
|
19
21
|
@objc public static let shared = BackgroundDeliveryManager()
|
|
20
22
|
|
|
21
23
|
private let healthStore = HKHealthStore()
|
|
@@ -37,18 +39,23 @@ private let bgLog = OSLog(subsystem: "com.kingstinct.healthkit", category: "Back
|
|
|
37
39
|
super.init()
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
+
/// Registers observer queries for any previously configured background delivery
|
|
43
|
+
/// types. Called at launch by `BackgroundLaunchHook.mm`; safe to call again.
|
|
42
44
|
@objc public func setupBackgroundObservers() {
|
|
43
45
|
guard HKHealthStore.isHealthDataAvailable() else { return }
|
|
44
46
|
|
|
45
47
|
guard let typeIdentifiers = UserDefaults.standard.stringArray(forKey: BackgroundDeliveryManager.typesKey) else {
|
|
48
|
+
os_log("launch: no background delivery types configured, nothing to register", log: bgLog, type: .debug)
|
|
46
49
|
return
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
let frequencyRaw = UserDefaults.standard.integer(forKey: BackgroundDeliveryManager.frequencyKey)
|
|
50
53
|
let frequency = HKUpdateFrequency(rawValue: frequencyRaw) ?? .immediate
|
|
51
54
|
|
|
55
|
+
os_log(
|
|
56
|
+
"launch: registering %d background observer(s): %{public}@", log: bgLog, type: .info,
|
|
57
|
+
typeIdentifiers.count, typeIdentifiers.joined(separator: ", "))
|
|
58
|
+
|
|
52
59
|
registerObservers(typeIdentifiers: typeIdentifiers, frequency: frequency)
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -250,7 +257,7 @@ private let bgLog = OSLog(subsystem: "com.kingstinct.healthkit", category: "Back
|
|
|
250
257
|
return HKSampleType.quantityType(forIdentifier: typeId)
|
|
251
258
|
}
|
|
252
259
|
if identifier.starts(with: "HKCategoryTypeIdentifier") {
|
|
253
|
-
let typeId =
|
|
260
|
+
let typeId = hkCategoryTypeIdentifier(fromIdentifierName: identifier)
|
|
254
261
|
return HKSampleType.categoryType(forIdentifier: typeId)
|
|
255
262
|
}
|
|
256
263
|
if identifier == "HKWorkoutTypeIdentifier" {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
/// Registers the launch-time observer setup without any AppDelegate changes.
|
|
5
|
+
///
|
|
6
|
+
/// `+load` runs when the binary is loaded, before `main`. It subscribes to
|
|
7
|
+
/// UIApplicationDidFinishLaunchingNotification, which UIKit posts synchronously
|
|
8
|
+
/// right after `application(_:didFinishLaunchingWithOptions:)` returns and
|
|
9
|
+
/// before the run loop continues, so observer queries are still registered
|
|
10
|
+
/// during launch as HealthKit background delivery requires. The manager is
|
|
11
|
+
/// looked up by name at runtime: importing this pod's Swift module from the
|
|
12
|
+
/// app's AppDelegate would drag NitroModules' C++ headers into a target
|
|
13
|
+
/// compiled without C++ interop and fail to build.
|
|
14
|
+
@interface BackgroundLaunchHook : NSObject
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
@implementation BackgroundLaunchHook
|
|
18
|
+
|
|
19
|
+
+ (void)load {
|
|
20
|
+
[[NSNotificationCenter defaultCenter]
|
|
21
|
+
addObserverForName:UIApplicationDidFinishLaunchingNotification
|
|
22
|
+
object:nil
|
|
23
|
+
queue:nil
|
|
24
|
+
usingBlock:^(NSNotification *_Nonnull note) {
|
|
25
|
+
Class managerClass = NSClassFromString(@"BackgroundDeliveryManager");
|
|
26
|
+
SEL sharedSelector = NSSelectorFromString(@"shared");
|
|
27
|
+
SEL setupSelector = NSSelectorFromString(@"setupBackgroundObservers");
|
|
28
|
+
if (managerClass == nil || ![managerClass respondsToSelector:sharedSelector]) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
#pragma clang diagnostic push
|
|
32
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
33
|
+
id shared = [managerClass performSelector:sharedSelector];
|
|
34
|
+
if ([shared respondsToSelector:setupSelector]) {
|
|
35
|
+
[shared performSelector:setupSelector];
|
|
36
|
+
}
|
|
37
|
+
#pragma clang diagnostic pop
|
|
38
|
+
}];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@end
|
package/ios/Constants.swift
CHANGED
|
@@ -34,3 +34,27 @@ let SpeedUnit = HKUnit(from: "m/s") // HKUnit.meter().unitDivided(by: HKUnit.se
|
|
|
34
34
|
let METUnit = HKUnit(from: "kcal/hr·kg")
|
|
35
35
|
|
|
36
36
|
let DEFAULT_QUERY_LIMIT = 20
|
|
37
|
+
|
|
38
|
+
/// Apple renamed `HKCategoryTypeIdentifierAudioExposureEvent` to
|
|
39
|
+
/// `HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent` in iOS 14, but the new
|
|
40
|
+
/// constant still carries the old raw string at runtime. Building the identifier from the
|
|
41
|
+
/// modern name as a raw string therefore makes `categoryType(forIdentifier:)` return nil,
|
|
42
|
+
/// and samples read back from HealthKit report the legacy name. These helpers translate
|
|
43
|
+
/// in both directions so JS only ever sees the modern identifier.
|
|
44
|
+
let HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent_NAME =
|
|
45
|
+
"HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent"
|
|
46
|
+
let HKCategoryTypeIdentifierAudioExposureEvent_NAME = "HKCategoryTypeIdentifierAudioExposureEvent"
|
|
47
|
+
|
|
48
|
+
func hkCategoryTypeIdentifier(fromIdentifierName name: String) -> HKCategoryTypeIdentifier {
|
|
49
|
+
if name == HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent_NAME {
|
|
50
|
+
return .environmentalAudioExposureEvent
|
|
51
|
+
}
|
|
52
|
+
return HKCategoryTypeIdentifier(rawValue: name)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func categoryTypeIdentifierName(fromHealthKitIdentifier identifier: String) -> String {
|
|
56
|
+
if identifier == HKCategoryTypeIdentifierAudioExposureEvent_NAME {
|
|
57
|
+
return HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent_NAME
|
|
58
|
+
}
|
|
59
|
+
return identifier
|
|
60
|
+
}
|
package/ios/CoreModule.swift
CHANGED
|
@@ -320,7 +320,11 @@ class CoreModule: HybridCoreModuleSpec {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
func isProtectedDataAvailableAsync() -> Promise<Bool> {
|
|
323
|
-
return Promise.
|
|
323
|
+
return Promise.async {
|
|
324
|
+
return await MainActor.run {
|
|
325
|
+
UIApplication.shared.isProtectedDataAvailable
|
|
326
|
+
}
|
|
327
|
+
}
|
|
324
328
|
}
|
|
325
329
|
|
|
326
330
|
func isHealthDataAvailable() -> Bool {
|
package/ios/Helpers.swift
CHANGED
|
@@ -172,7 +172,7 @@ func deserializeHKQueryAnchor(base64String: String?) throws -> HKQueryAnchor? {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
func initializeCategoryType(_ identifier: String) throws -> HKCategoryType {
|
|
175
|
-
let identifier =
|
|
175
|
+
let identifier = hkCategoryTypeIdentifier(fromIdentifierName: identifier)
|
|
176
176
|
if let sampleType = HKSampleType.categoryType(forIdentifier: identifier) {
|
|
177
177
|
return sampleType
|
|
178
178
|
}
|
|
@@ -483,3 +483,31 @@ func buildStatisticsOptions(statistics: [StatisticsOptions], quantityType: HKQua
|
|
|
483
483
|
}
|
|
484
484
|
return opts
|
|
485
485
|
}
|
|
486
|
+
|
|
487
|
+
private let storeReopenRetryDelay: UInt64 = 1_000_000_000
|
|
488
|
+
|
|
489
|
+
/// HealthKit seals its store the moment the device locks and re-opens it a beat
|
|
490
|
+
/// after it unlocks, and iOS can resume the app inside that beat. A query that
|
|
491
|
+
/// hits the gap fails with `errorDatabaseInaccessible` although protected data is
|
|
492
|
+
/// reported available — the store is re-opening, not sealed. Runs `operation`
|
|
493
|
+
/// and, on exactly that refusal, gives it one more attempt after a short delay.
|
|
494
|
+
/// A refusal while protected data is unavailable is the sealed store itself and
|
|
495
|
+
/// is thrown as-is, as is every other error.
|
|
496
|
+
func retryingWhileStoreReopens<T>(
|
|
497
|
+
_ operation: () async throws -> T
|
|
498
|
+
) async throws -> T {
|
|
499
|
+
do {
|
|
500
|
+
return try await operation()
|
|
501
|
+
} catch {
|
|
502
|
+
let nsError = error as NSError
|
|
503
|
+
guard nsError.domain == HKError.errorDomain,
|
|
504
|
+
nsError.code == HKError.Code.errorDatabaseInaccessible.rawValue
|
|
505
|
+
else { throw error }
|
|
506
|
+
let protectedDataAvailable = await MainActor.run {
|
|
507
|
+
UIApplication.shared.isProtectedDataAvailable
|
|
508
|
+
}
|
|
509
|
+
guard protectedDataAvailable else { throw error }
|
|
510
|
+
try await Task.sleep(nanoseconds: storeReopenRetryDelay)
|
|
511
|
+
return try await operation()
|
|
512
|
+
}
|
|
513
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MemoryEstimation.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Rough estimates of the native heap kept alive by HybridObject proxies,
|
|
6
|
+
// reported to Nitro via `memorySize` so the JS garbage collector can account
|
|
7
|
+
// for memory it cannot see. Precision is not the goal; the right order of
|
|
8
|
+
// magnitude per object is.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import HealthKit
|
|
12
|
+
|
|
13
|
+
/// Approximate per-object bridging overhead of a Nitro HybridObject that Nitro
|
|
14
|
+
/// does not account for itself. The generated `*Spec_cxx.swift` adds the Swift
|
|
15
|
+
/// implementation instance via `MemoryHelper.getSizeOf`; this covers the rest:
|
|
16
|
+
/// the Swift `_cxx` bridge class, the C++ `HybridObject` with its per-runtime
|
|
17
|
+
/// cache map, the `shared_ptr` control block and the reference-state records
|
|
18
|
+
/// behind the JS weak reference. Add it once, in each proxy's `memorySize`.
|
|
19
|
+
let nitroHybridObjectOverheadBytes = 512
|
|
20
|
+
|
|
21
|
+
/// Rough size of an `NSString` on the heap, including its object header.
|
|
22
|
+
func estimateStringMemorySize(_ value: String?) -> Int {
|
|
23
|
+
guard let value = value else {
|
|
24
|
+
return 0
|
|
25
|
+
}
|
|
26
|
+
return 32 + value.utf16.count * 2
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Rough size of a `metadata` dictionary as HealthKit keeps it in memory.
|
|
30
|
+
func estimateMetadataMemorySize(_ metadata: [String: Any]?) -> Int {
|
|
31
|
+
guard let metadata = metadata, !metadata.isEmpty else {
|
|
32
|
+
return 0
|
|
33
|
+
}
|
|
34
|
+
// NSDictionary header plus one bucket per entry.
|
|
35
|
+
var total = 64 + metadata.count * 48
|
|
36
|
+
for (key, value) in metadata {
|
|
37
|
+
total += estimateStringMemorySize(key)
|
|
38
|
+
if let string = value as? String {
|
|
39
|
+
total += estimateStringMemorySize(string)
|
|
40
|
+
} else if value is HKQuantity {
|
|
41
|
+
// HKQuantity: object header, unit object, double.
|
|
42
|
+
total += 128
|
|
43
|
+
} else {
|
|
44
|
+
// NSNumber, NSDate and friends are small tagged or boxed objects.
|
|
45
|
+
total += 32
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return total
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Rough size of one `HKStatistics` entry: the object itself plus the
|
|
52
|
+
/// sum/min/max/average/most-recent quantities and their date interval.
|
|
53
|
+
let estimatedStatisticsEntryMemorySize = 512
|
|
54
|
+
|
|
55
|
+
/// Estimate of the `HKWorkout` object graph only; the proxy adds Nitro's
|
|
56
|
+
/// bridging overhead on top.
|
|
57
|
+
func estimateWorkoutMemorySize(_ workout: HKWorkout) -> Int {
|
|
58
|
+
// HKWorkout object, uuid, dates, HKSourceRevision (+HKSource), HKDevice,
|
|
59
|
+
// and the four total quantities.
|
|
60
|
+
var total = 1_024
|
|
61
|
+
total += estimateMetadataMemorySize(workout.metadata)
|
|
62
|
+
|
|
63
|
+
if let events = workout.workoutEvents {
|
|
64
|
+
for event in events {
|
|
65
|
+
// HKWorkoutEvent: object header, type, NSDateInterval.
|
|
66
|
+
total += 128 + estimateMetadataMemorySize(event.metadata)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if #available(iOS 16.0, *) {
|
|
71
|
+
total += workout.allStatistics.count * estimatedStatisticsEntryMemorySize
|
|
72
|
+
|
|
73
|
+
for activity in workout.workoutActivities {
|
|
74
|
+
// HKWorkoutActivity: object header, uuid, dates, HKWorkoutConfiguration.
|
|
75
|
+
total += 512
|
|
76
|
+
total += estimateMetadataMemorySize(activity.metadata)
|
|
77
|
+
total += activity.workoutEvents.count * 128
|
|
78
|
+
total += activity.allStatistics.count * estimatedStatisticsEntryMemorySize
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return total
|
|
83
|
+
}
|
|
@@ -53,7 +53,7 @@ func queryStatisticsForQuantityInternal(
|
|
|
53
53
|
|
|
54
54
|
func serializeStatistics(gottenStats: HKStatistics, unit: HKUnit) -> QueryStatisticsResponse {
|
|
55
55
|
let sources = gottenStats.sources?.map { source in
|
|
56
|
-
return
|
|
56
|
+
return serializeSourceStruct(source)
|
|
57
57
|
} ?? []
|
|
58
58
|
|
|
59
59
|
var averageQuantity: Quantity?
|
|
@@ -244,7 +244,7 @@ func serializeStatisticsPerSource(gottenStats: HKStatistics, unit: HKUnit)
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
return QueryStatisticsResponseFromSingleSource(
|
|
247
|
-
source:
|
|
247
|
+
source: serializeSourceStruct(source),
|
|
248
248
|
startDate: gottenStats.startDate,
|
|
249
249
|
endDate: gottenStats.endDate,
|
|
250
250
|
duration: duration,
|
|
@@ -313,11 +313,13 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec {
|
|
|
313
313
|
return Promise.async {
|
|
314
314
|
let quantityType = try initializeQuantityType(identifier.stringValue)
|
|
315
315
|
|
|
316
|
-
if let gottenStats = try await
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
316
|
+
if let gottenStats = try await retryingWhileStoreReopens({
|
|
317
|
+
try await queryStatisticsForQuantityInternal(
|
|
318
|
+
quantityType: quantityType,
|
|
319
|
+
statistics: statistics,
|
|
320
|
+
options: options
|
|
321
|
+
)
|
|
322
|
+
}) {
|
|
321
323
|
let unit = try await getUnitToUse(
|
|
322
324
|
unitOverride: options?.unit,
|
|
323
325
|
quantityType: quantityType
|
|
@@ -336,13 +338,15 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec {
|
|
|
336
338
|
return Promise.async {
|
|
337
339
|
let quantityType = try initializeQuantityType(identifier.stringValue)
|
|
338
340
|
|
|
339
|
-
if let statistics = try await
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
341
|
+
if let statistics = try await retryingWhileStoreReopens({
|
|
342
|
+
try await queryStatisticsCollectionForQuantityInternal(
|
|
343
|
+
quantityType: quantityType,
|
|
344
|
+
statistics: statistics,
|
|
345
|
+
anchorDate: anchorDate,
|
|
346
|
+
intervalComponents: intervalComponents,
|
|
347
|
+
options: options
|
|
348
|
+
)
|
|
349
|
+
}) {
|
|
346
350
|
|
|
347
351
|
let unit = try await getUnitToUse(
|
|
348
352
|
unitOverride: options?.unit,
|
|
@@ -385,11 +389,13 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec {
|
|
|
385
389
|
return Promise.async {
|
|
386
390
|
let quantityType = try initializeQuantityType(identifier.stringValue)
|
|
387
391
|
|
|
388
|
-
if let gottenStats = try await
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
392
|
+
if let gottenStats = try await retryingWhileStoreReopens({
|
|
393
|
+
try await queryStatisticsForQuantityInternal(
|
|
394
|
+
quantityType: quantityType,
|
|
395
|
+
statistics: statistics,
|
|
396
|
+
options: options
|
|
397
|
+
)
|
|
398
|
+
}) {
|
|
393
399
|
let unit = try await getUnitToUse(
|
|
394
400
|
unitOverride: options?.unit,
|
|
395
401
|
quantityType: quantityType
|
|
@@ -412,13 +418,15 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec {
|
|
|
412
418
|
return Promise.async {
|
|
413
419
|
let quantityType = try initializeQuantityType(identifier.stringValue)
|
|
414
420
|
|
|
415
|
-
if let statistics = try await
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
421
|
+
if let statistics = try await retryingWhileStoreReopens({
|
|
422
|
+
try await queryStatisticsCollectionForQuantityInternal(
|
|
423
|
+
quantityType: quantityType,
|
|
424
|
+
statistics: statistics,
|
|
425
|
+
anchorDate: anchorDate,
|
|
426
|
+
intervalComponents: intervalComponents,
|
|
427
|
+
options: options
|
|
428
|
+
)
|
|
429
|
+
}) {
|
|
422
430
|
let unit = try await getUnitToUse(
|
|
423
431
|
unitOverride: options?.unit,
|
|
424
432
|
quantityType: quantityType
|