@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.
Files changed (71) hide show
  1. package/README.md +32 -2
  2. package/app.plugin.js +49 -94
  3. package/app.plugin.ts +6 -56
  4. package/ios/BackgroundDeliveryManager.swift +13 -6
  5. package/ios/BackgroundLaunchHook.mm +41 -0
  6. package/ios/Constants.swift +24 -0
  7. package/ios/CoreModule.swift +5 -1
  8. package/ios/Helpers.swift +29 -1
  9. package/ios/MemoryEstimation.swift +83 -0
  10. package/ios/QuantityTypeModule.swift +34 -26
  11. package/ios/Serializers.swift +18 -3
  12. package/ios/SourceProxy.swift +14 -5
  13. package/ios/WorkoutProxy.swift +28 -11
  14. package/lib/commonjs/generated/healthkit.generated.js +13 -3
  15. package/lib/commonjs/healthkit.ios.js +3 -1
  16. package/lib/commonjs/healthkit.js +4 -2
  17. package/lib/module/generated/healthkit.generated.js +12 -2
  18. package/lib/module/healthkit.ios.js +2 -0
  19. package/lib/module/healthkit.js +2 -0
  20. package/lib/typescript/generated/healthkit.generated.d.ts +17 -7
  21. package/lib/typescript/healthkit.d.ts +1 -0
  22. package/lib/typescript/healthkit.ios.d.ts +2 -0
  23. package/lib/typescript/specs/CoreModule.nitro.d.ts +6 -4
  24. package/lib/typescript/specs/SourceProxy.nitro.d.ts +1 -2
  25. package/lib/typescript/types/QuantityType.d.ts +3 -3
  26. package/lib/typescript/types/Source.d.ts +2 -2
  27. package/lib/typescript/types/Units.d.ts +1 -1
  28. package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.cpp +16 -16
  29. package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.hpp +23 -12
  30. package/nitrogen/generated/ios/c++/HybridCategoryTypeModuleSpecSwift.hpp +7 -4
  31. package/nitrogen/generated/ios/c++/HybridCorrelationTypeModuleSpecSwift.hpp +7 -4
  32. package/nitrogen/generated/ios/c++/HybridElectrocardiogramModuleSpecSwift.hpp +7 -4
  33. package/nitrogen/generated/ios/c++/HybridHeartbeatSeriesModuleSpecSwift.hpp +7 -4
  34. package/nitrogen/generated/ios/c++/HybridMedicationModuleSpecSwift.hpp +7 -4
  35. package/nitrogen/generated/ios/c++/HybridQuantityTypeModuleSpecSwift.hpp +7 -4
  36. package/nitrogen/generated/ios/c++/HybridStateOfMindModuleSpecSwift.hpp +7 -4
  37. package/nitrogen/generated/ios/c++/HybridWorkoutProxySpecSwift.hpp +3 -4
  38. package/nitrogen/generated/ios/c++/HybridWorkoutsModuleSpecSwift.hpp +6 -3
  39. package/nitrogen/generated/ios/swift/CategoryTypeIdentifier.swift +8 -0
  40. package/nitrogen/generated/ios/swift/CategoryTypeIdentifierWriteable.swift +8 -0
  41. package/nitrogen/generated/ios/swift/ObjectTypeIdentifier.swift +8 -4
  42. package/nitrogen/generated/ios/swift/QuantityTypeIdentifier.swift +0 -4
  43. package/nitrogen/generated/ios/swift/QuantityTypeIdentifierWriteable.swift +0 -4
  44. package/nitrogen/generated/ios/swift/QueryStatisticsResponse.swift +6 -13
  45. package/nitrogen/generated/ios/swift/QueryStatisticsResponseFromSingleSource.swift +4 -11
  46. package/nitrogen/generated/ios/swift/SampleTypeIdentifier.swift +8 -4
  47. package/nitrogen/generated/ios/swift/SampleTypeIdentifierWriteable.swift +8 -4
  48. package/nitrogen/generated/ios/swift/SourceRevision.swift +4 -11
  49. package/nitrogen/generated/ios/swift/WorkoutActivityType.swift +8 -0
  50. package/nitrogen/generated/shared/c++/CategoryTypeIdentifier.hpp +67 -59
  51. package/nitrogen/generated/shared/c++/CategoryTypeIdentifierWriteable.hpp +67 -59
  52. package/nitrogen/generated/shared/c++/ObjectTypeIdentifier.hpp +202 -198
  53. package/nitrogen/generated/shared/c++/QuantityTypeIdentifier.hpp +107 -111
  54. package/nitrogen/generated/shared/c++/QuantityTypeIdentifierWriteable.hpp +107 -111
  55. package/nitrogen/generated/shared/c++/QueryStatisticsResponse.hpp +8 -9
  56. package/nitrogen/generated/shared/c++/QueryStatisticsResponseFromSingleSource.hpp +8 -9
  57. package/nitrogen/generated/shared/c++/SampleTypeIdentifier.hpp +195 -191
  58. package/nitrogen/generated/shared/c++/SampleTypeIdentifierWriteable.hpp +189 -185
  59. package/nitrogen/generated/shared/c++/SourceRevision.hpp +8 -9
  60. package/nitrogen/generated/shared/c++/WorkoutActivityType.hpp +4 -0
  61. package/package.json +5 -3
  62. package/src/generated/healthkit-schema.json +45 -8
  63. package/src/generated/healthkit.generated.ts +18 -6
  64. package/src/healthkit.ios.ts +3 -0
  65. package/src/healthkit.ts +5 -0
  66. package/src/specs/CoreModule.nitro.ts +6 -4
  67. package/src/specs/SourceProxy.nitro.ts +1 -1
  68. package/src/type-tests/generated-typing.ts +30 -8
  69. package/src/types/QuantityType.ts +3 -3
  70. package/src/types/Source.ts +2 -2
  71. 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
- const {
2
- withPlugins,
3
- createRunOncePlugin,
4
- withEntitlementsPlist,
5
- withInfoPlist,
6
- } = require('@expo/config-plugins')
7
-
8
- /**
9
- * @typedef ConfigPlugin
10
- * @type {import('@expo/config-plugins').ConfigPlugin<T>}
11
- * @template T = void
12
- */
13
-
14
- /**
15
- * @typedef InfoPlistConfig
16
- * @type {{
17
- * NSHealthShareUsageDescription?: string | boolean,
18
- * NSHealthUpdateUsageDescription?: string | boolean
19
- * }}
20
- */
21
-
22
- /**
23
- * @typedef AppPluginConfig
24
- * @type {InfoPlistConfig & { background?: boolean }}
25
- */
26
-
27
- /**
28
- * @type {ConfigPlugin<{background: boolean}>}
29
- */
30
- const withEntitlementsPlugin = (
31
- config,
32
- /**
33
- * @type {{background: boolean} | undefined}
34
- * */
35
- props,
36
- ) =>
37
- withEntitlementsPlist(config, (config) => {
38
- config.modResults['com.apple.developer.healthkit'] = true
39
-
40
- // background is enabled by default, but possible to opt-out from
41
- // (haven't seen any drawbacks from having it enabled)
42
- if (props?.background !== false) {
43
- config.modResults['com.apple.developer.healthkit.background-delivery'] =
44
- true
45
- }
46
-
47
- return config
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
- /// Usage from AppDelegate.swift:
13
- /// BackgroundDeliveryManager.shared.setupBackgroundObservers()
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 public class BackgroundDeliveryManager: NSObject {
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
- /// Call this from AppDelegate.didFinishLaunchingWithOptions to register observer queries
41
- /// for any previously configured background delivery types.
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 = HKCategoryTypeIdentifier(rawValue: identifier)
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
@@ -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
+ }
@@ -320,7 +320,11 @@ class CoreModule: HybridCoreModuleSpec {
320
320
  }
321
321
 
322
322
  func isProtectedDataAvailableAsync() -> Promise<Bool> {
323
- return Promise.resolved(withResult: UIApplication.shared.isProtectedDataAvailable)
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 = HKCategoryTypeIdentifier(rawValue: 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 serializeSource(source)
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: serializeSource(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 queryStatisticsForQuantityInternal(
317
- quantityType: quantityType,
318
- statistics: statistics,
319
- options: options
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 queryStatisticsCollectionForQuantityInternal(
340
- quantityType: quantityType,
341
- statistics: statistics,
342
- anchorDate: anchorDate,
343
- intervalComponents: intervalComponents,
344
- options: options
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 queryStatisticsForQuantityInternal(
389
- quantityType: quantityType,
390
- statistics: statistics,
391
- options: options
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 queryStatisticsCollectionForQuantityInternal(
416
- quantityType: quantityType,
417
- statistics: statistics,
418
- anchorDate: anchorDate,
419
- intervalComponents: intervalComponents,
420
- options: options
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