@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.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/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,683 +1,529 @@
|
|
|
1
|
-
# HealthKit
|
|
1
|
+
# HealthKit patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Extended examples that go with the main skill. Swift 6.3, iOS 26 unless noted.
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
7
|
-
- [Workout
|
|
8
|
-
- [Platform and
|
|
9
|
-
- [Background
|
|
10
|
-
- [Anchored
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
7
|
+
- [Workout sessions in depth](#workout-sessions-in-depth)
|
|
8
|
+
- [Platform and authorization edge cases](#platform-and-authorization-edge-cases)
|
|
9
|
+
- [Background delivery in detail](#background-delivery-in-detail)
|
|
10
|
+
- [Anchored object queries](#anchored-object-queries)
|
|
11
|
+
- [Predicates](#predicates)
|
|
12
|
+
- [Chart data](#chart-data)
|
|
13
|
+
- [SwiftUI integration](#swiftui-integration)
|
|
14
|
+
- [Characteristics](#characteristics)
|
|
15
15
|
|
|
16
|
-
## Workout
|
|
16
|
+
## Workout sessions in depth
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Availability repeats here because it drives the design: `HKWorkoutSession` is
|
|
19
|
+
iOS and iPadOS 17+, visionOS 1+ and watchOS 2+, while `HKLiveWorkoutBuilder`
|
|
20
|
+
is iOS and iPadOS 26+ and watchOS 5+. If the app still supports iOS or iPadOS
|
|
21
|
+
before 26, gate the live builder and fall back to building and saving the
|
|
22
|
+
workout after the fact instead of collecting it live.
|
|
22
23
|
|
|
23
|
-
iPhone and iPad
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
surface.
|
|
24
|
+
iPhone and iPad produce no live heart-rate samples by themselves; an external
|
|
25
|
+
Bluetooth heart-rate sensor is needed. Phones also tend to lock partway through
|
|
26
|
+
a workout, so if metrics should show on the Lock Screen, design around the
|
|
27
|
+
system prompt that grants workout-data access there and around the Live
|
|
28
|
+
Activity that displays it.
|
|
29
29
|
|
|
30
|
-
###
|
|
30
|
+
### A complete manager
|
|
31
31
|
|
|
32
32
|
```swift
|
|
33
33
|
import HealthKit
|
|
34
|
+
import Observation
|
|
34
35
|
|
|
36
|
+
@available(iOS 26.0, *)
|
|
35
37
|
@Observable
|
|
36
38
|
@MainActor
|
|
37
|
-
final class
|
|
38
|
-
let
|
|
39
|
-
|
|
39
|
+
final class SwimTracker: NSObject {
|
|
40
|
+
private let store: HKHealthStore
|
|
40
41
|
private var session: HKWorkoutSession?
|
|
41
42
|
private var builder: HKLiveWorkoutBuilder?
|
|
42
43
|
|
|
43
|
-
var
|
|
44
|
-
var
|
|
45
|
-
var
|
|
46
|
-
var
|
|
47
|
-
var
|
|
48
|
-
|
|
49
|
-
func startWorkout(activityType: HKWorkoutActivityType) async throws {
|
|
50
|
-
let configuration = HKWorkoutConfiguration()
|
|
51
|
-
configuration.activityType = activityType
|
|
52
|
-
configuration.locationType = .outdoor
|
|
53
|
-
|
|
54
|
-
let session = try HKWorkoutSession(
|
|
55
|
-
healthStore: healthStore,
|
|
56
|
-
configuration: configuration
|
|
57
|
-
)
|
|
58
|
-
let builder = session.associatedWorkoutBuilder()
|
|
59
|
-
|
|
60
|
-
builder.dataSource = HKLiveWorkoutDataSource(
|
|
61
|
-
healthStore: healthStore,
|
|
62
|
-
workoutConfiguration: configuration
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
session.delegate = self
|
|
66
|
-
builder.delegate = self
|
|
67
|
-
|
|
68
|
-
self.session = session
|
|
69
|
-
self.builder = builder
|
|
44
|
+
private(set) var pulse: Double = 0
|
|
45
|
+
private(set) var kilocalories: Double = 0
|
|
46
|
+
private(set) var metres: Double = 0
|
|
47
|
+
private(set) var elapsed: TimeInterval = 0
|
|
48
|
+
private(set) var isRunning = false
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
isActive = true
|
|
50
|
+
init(store: HKHealthStore) {
|
|
51
|
+
self.store = store
|
|
74
52
|
}
|
|
75
53
|
|
|
76
|
-
func
|
|
77
|
-
|
|
54
|
+
func start() async throws {
|
|
55
|
+
let plan = HKWorkoutConfiguration()
|
|
56
|
+
plan.activityType = .swimming
|
|
57
|
+
plan.swimmingLocationType = .pool
|
|
58
|
+
plan.lapLength = HKQuantity(unit: .meter(), doubleValue: 25)
|
|
59
|
+
|
|
60
|
+
let newSession = try HKWorkoutSession(healthStore: store, configuration: plan)
|
|
61
|
+
let newBuilder = newSession.associatedWorkoutBuilder()
|
|
62
|
+
newSession.delegate = self
|
|
63
|
+
newBuilder.delegate = self
|
|
64
|
+
newBuilder.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: plan)
|
|
65
|
+
session = newSession
|
|
66
|
+
builder = newBuilder
|
|
67
|
+
|
|
68
|
+
let startDate = Date()
|
|
69
|
+
newSession.startActivity(with: startDate)
|
|
70
|
+
try await newBuilder.beginCollection(at: startDate)
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
func
|
|
81
|
-
|
|
73
|
+
func pause() { session?.pause() }
|
|
74
|
+
func resume() { session?.resume() }
|
|
75
|
+
|
|
76
|
+
func stop() async throws {
|
|
77
|
+
guard let activeSession = session, let activeBuilder = builder else { return }
|
|
78
|
+
activeSession.end()
|
|
79
|
+
try await activeBuilder.endCollection(at: .now)
|
|
80
|
+
_ = try await activeBuilder.finishWorkout()
|
|
81
|
+
session = nil
|
|
82
|
+
builder = nil
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
func
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
fileprivate func apply(_ stats: HKStatistics, for type: HKQuantityType) {
|
|
86
|
+
switch type {
|
|
87
|
+
case HKQuantityType(.heartRate):
|
|
88
|
+
pulse = stats.mostRecentQuantity()?.doubleValue(for: .count().unitDivided(by: .minute())) ?? pulse
|
|
89
|
+
case HKQuantityType(.activeEnergyBurned):
|
|
90
|
+
kilocalories = stats.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? kilocalories
|
|
91
|
+
case HKQuantityType(.distanceSwimming):
|
|
92
|
+
metres = stats.sumQuantity()?.doubleValue(for: .meter()) ?? metres
|
|
93
|
+
default:
|
|
94
|
+
break
|
|
95
|
+
}
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
_ workoutSession: HKWorkoutSession,
|
|
100
|
-
didChangeTo toState: HKWorkoutSessionState,
|
|
101
|
-
from fromState: HKWorkoutSessionState,
|
|
102
|
-
date: Date
|
|
103
|
-
) {
|
|
99
|
+
@available(iOS 26.0, *)
|
|
100
|
+
extension SwimTracker: HKWorkoutSessionDelegate {
|
|
101
|
+
nonisolated func workoutSession(_ sender: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState,
|
|
102
|
+
from fromState: HKWorkoutSessionState, date: Date) {
|
|
104
103
|
Task { @MainActor in
|
|
105
104
|
switch toState {
|
|
106
|
-
case .running:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
isActive = false
|
|
110
|
-
case .ended, .stopped:
|
|
111
|
-
isActive = false
|
|
112
|
-
default:
|
|
113
|
-
break
|
|
105
|
+
case .running: self.isRunning = true
|
|
106
|
+
case .paused, .ended, .stopped: self.isRunning = false
|
|
107
|
+
default: break
|
|
114
108
|
}
|
|
115
109
|
}
|
|
116
110
|
}
|
|
117
111
|
|
|
118
|
-
nonisolated func workoutSession(
|
|
119
|
-
|
|
120
|
-
didFailWithError error: Error
|
|
121
|
-
) {
|
|
122
|
-
Task { @MainActor in
|
|
123
|
-
print("Workout session failed: \(error)")
|
|
124
|
-
isActive = false
|
|
125
|
-
}
|
|
112
|
+
nonisolated func workoutSession(_ sender: HKWorkoutSession, didFailWithError failure: Error) {
|
|
113
|
+
Task { @MainActor in self.isRunning = false }
|
|
126
114
|
}
|
|
127
115
|
}
|
|
128
116
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
_ workoutBuilder: HKLiveWorkoutBuilder
|
|
134
|
-
) {
|
|
135
|
-
// Handle workout events (pause, resume, lap, etc.)
|
|
117
|
+
@available(iOS 26.0, *)
|
|
118
|
+
extension SwimTracker: HKLiveWorkoutBuilderDelegate {
|
|
119
|
+
nonisolated func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
|
|
120
|
+
// Pause, resume and lap markers arrive here via workoutBuilder.workoutEvents.
|
|
136
121
|
}
|
|
137
122
|
|
|
138
|
-
nonisolated func workoutBuilder(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
123
|
+
nonisolated func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
|
|
124
|
+
let time = workoutBuilder.elapsedTime
|
|
125
|
+
let snapshot = collectedTypes.compactMap { $0 as? HKQuantityType }
|
|
126
|
+
.compactMap { type in workoutBuilder.statistics(for: type).map { (type, $0) } }
|
|
142
127
|
Task { @MainActor in
|
|
143
|
-
|
|
144
|
-
|
|
128
|
+
self.elapsed = time
|
|
129
|
+
for (type, stats) in snapshot { self.apply(stats, for: type) }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
145
134
|
|
|
146
|
-
|
|
135
|
+
Points to notice:
|
|
147
136
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
137
|
+
- Both delegates are set: the session's for state changes and the builder's for
|
|
138
|
+
data.
|
|
139
|
+
- `pause()` and `resume()` go through the session.
|
|
140
|
+
- Ending runs `end()`, `endCollection(at:)`, `finishWorkout()` and then drops
|
|
141
|
+
the references.
|
|
142
|
+
- Delegate callbacks are `nonisolated` and hop back with
|
|
143
|
+
`Task { @MainActor in ... }`. Session states to handle include `.running`,
|
|
144
|
+
`.paused`, `.ended` and `.stopped`.
|
|
145
|
+
- In `didCollectDataOf`, cast each type to `HKQuantityType` and read
|
|
146
|
+
`statistics(for:)`: `mostRecentQuantity()` for heart rate in beats per
|
|
147
|
+
minute, `sumQuantity()` for energy in kilocalories and for distance in
|
|
148
|
+
metres. `elapsedTime` on the builder is the running clock.
|
|
152
149
|
|
|
153
|
-
|
|
154
|
-
activeCalories = statistics?.sumQuantity()?
|
|
155
|
-
.doubleValue(for: .kilocalorie()) ?? 0
|
|
150
|
+
### Mirroring a watch workout to iPhone
|
|
156
151
|
|
|
157
|
-
|
|
158
|
-
distance = statistics?.sumQuantity()?
|
|
159
|
-
.doubleValue(for: .meter()) ?? 0
|
|
152
|
+
The watch runs the primary session and starts mirroring:
|
|
160
153
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
}
|
|
154
|
+
```swift
|
|
155
|
+
import HealthKit
|
|
165
156
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
157
|
+
#if os(watchOS)
|
|
158
|
+
func mirrorToPhone(_ primary: HKWorkoutSession) async throws {
|
|
159
|
+
try await primary.startMirroringToCompanionDevice()
|
|
169
160
|
}
|
|
161
|
+
#endif
|
|
170
162
|
```
|
|
171
163
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
Start mirroring from the primary watchOS session. In the iOS companion app,
|
|
175
|
-
assign `workoutSessionMirroringStartHandler` as the app launches so it can
|
|
176
|
-
receive mirrored sessions even when launched in the background. The handler runs
|
|
177
|
-
on an arbitrary background queue and may be called more than once if the devices
|
|
178
|
-
disconnect and reconnect during a workout.
|
|
164
|
+
The iPhone app registers its handler at launch, early enough that a mirrored
|
|
165
|
+
session still arrives when the system launches the app in the background:
|
|
179
166
|
|
|
180
167
|
```swift
|
|
181
|
-
|
|
182
|
-
func startMirroring() async throws {
|
|
183
|
-
try await session?.startMirroringToCompanionDevice()
|
|
184
|
-
}
|
|
168
|
+
import HealthKit
|
|
185
169
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
170
|
+
@available(iOS 26.0, *)
|
|
171
|
+
final class CompanionReceiver: NSObject, Sendable, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate {
|
|
172
|
+
let store = HKHealthStore()
|
|
173
|
+
|
|
174
|
+
func install() {
|
|
175
|
+
store.workoutSessionMirroringStartHandler = { [weak self] mirrored in
|
|
176
|
+
guard let self, mirrored.type == .mirrored else { return }
|
|
177
|
+
mirrored.delegate = self
|
|
178
|
+
mirrored.associatedWorkoutBuilder().delegate = self
|
|
179
|
+
}
|
|
193
180
|
}
|
|
194
|
-
}
|
|
195
181
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
182
|
+
func ping(_ session: HKWorkoutSession, payload: Data) async throws {
|
|
183
|
+
try await session.sendToRemoteWorkoutSession(data: payload)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
func workoutSession(_ s: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState,
|
|
187
|
+
from fromState: HKWorkoutSessionState, date: Date) {}
|
|
188
|
+
func workoutSession(_ s: HKWorkoutSession, didFailWithError failure: Error) {}
|
|
189
|
+
func workoutSession(_ s: HKWorkoutSession, didReceiveDataFromRemoteWorkoutSession data: [Data]) {}
|
|
190
|
+
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {}
|
|
191
|
+
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {}
|
|
199
192
|
}
|
|
200
193
|
```
|
|
201
194
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
- People
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- In
|
|
220
|
-
|
|
221
|
-
best-effort unless the user explicitly
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
## Background
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
195
|
+
- The handler is called on an arbitrary background queue.
|
|
196
|
+
- It can fire more than once, for example after the devices disconnect and
|
|
197
|
+
reconnect, so make it idempotent.
|
|
198
|
+
- The session it receives is an ordinary `HKWorkoutSession` whose `type` is
|
|
199
|
+
`.mirrored`. Set the session delegate and the delegate of its
|
|
200
|
+
`associatedWorkoutBuilder()`.
|
|
201
|
+
- Either side sends custom data with `sendToRemoteWorkoutSession(data:)`.
|
|
202
|
+
|
|
203
|
+
## Platform and authorization edge cases
|
|
204
|
+
|
|
205
|
+
- Turning on the capability can add `healthkit` to
|
|
206
|
+
`UIRequiredDeviceCapabilities`, which stops the app from installing on
|
|
207
|
+
devices without Health. Remove the entry only when HealthKit really is
|
|
208
|
+
optional and the app offers something useful without it.
|
|
209
|
+
- People change permissions later in Settings or in the Health app. Re-check
|
|
210
|
+
before permission-dependent UI and write paths instead of trusting the answer
|
|
211
|
+
from the first request.
|
|
212
|
+
- In a Guest User session on Apple Vision Pro, data the owner already allowed
|
|
213
|
+
may be readable while new authorization requests and writes fail. Treat
|
|
214
|
+
writes as best-effort there, unless the user explicitly asked to save, in
|
|
215
|
+
which case explain what happened.
|
|
216
|
+
|
|
217
|
+
## Background delivery in detail
|
|
218
|
+
|
|
219
|
+
- Entitlement: `com.apple.developer.healthkit.background-delivery`, added by
|
|
220
|
+
the HealthKit Background Delivery capability option.
|
|
221
|
+
- An executed `HKObserverQuery` for the same sample type must exist.
|
|
222
|
+
- `HKUpdateFrequency` is an upper bound, not a promise.
|
|
223
|
+
- On iOS, step count is delivered at most hourly even if `.immediate` is
|
|
224
|
+
requested.
|
|
225
|
+
- Background server queries do not run in Simulator.
|
|
226
|
+
- If the completion handler is not called, HealthKit backs off and, after
|
|
227
|
+
repeated misses, may stop delivering.
|
|
231
228
|
|
|
232
229
|
```swift
|
|
233
|
-
|
|
234
|
-
let stepType = HKQuantityType(.stepCount)
|
|
230
|
+
import HealthKit
|
|
235
231
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
) { _, completionHandler, error in
|
|
232
|
+
func startSleepSync(store: HKHealthStore, sync: @escaping @Sendable () async -> Void) async throws {
|
|
233
|
+
let sleep = HKCategoryType(.sleepAnalysis)
|
|
234
|
+
let observer = HKObserverQuery(sampleType: sleep, predicate: nil) { _, completionHandler, error in
|
|
240
235
|
defer { completionHandler() }
|
|
241
|
-
|
|
242
|
-
Task {
|
|
243
|
-
// Run an anchored query or statistics refresh here.
|
|
244
|
-
}
|
|
236
|
+
if error != nil { return }
|
|
237
|
+
Task { await sync() }
|
|
245
238
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
try await healthStore.enableBackgroundDelivery(
|
|
250
|
-
for: stepType,
|
|
251
|
-
frequency: .hourly
|
|
252
|
-
)
|
|
239
|
+
store.execute(observer)
|
|
240
|
+
try await store.enableBackgroundDelivery(for: sleep, frequency: .hourly)
|
|
253
241
|
}
|
|
254
242
|
```
|
|
255
243
|
|
|
256
|
-
|
|
257
|
-
types have tighter system caps; for example, iOS step-count background delivery
|
|
258
|
-
is capped at hourly even if `.immediate` is requested. Background server queries
|
|
259
|
-
are not supported on Simulator, so validate delivery on real hardware. If the
|
|
260
|
-
observer completion handler is not called, HealthKit backs off and can stop
|
|
261
|
-
sending background updates after repeated failures.
|
|
244
|
+
The `sync` closure is where an anchored query or a statistics refresh runs.
|
|
262
245
|
|
|
263
|
-
## Anchored
|
|
246
|
+
## Anchored object queries
|
|
264
247
|
|
|
265
|
-
|
|
248
|
+
`HKAnchoredObjectQuery` returns only what changed (added or deleted) since the
|
|
249
|
+
last `HKQueryAnchor`.
|
|
266
250
|
|
|
267
251
|
```swift
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
private let healthStore = HKHealthStore()
|
|
271
|
-
private var anchor: HKQueryAnchor?
|
|
272
|
-
private var observerQuery: HKObserverQuery?
|
|
252
|
+
import HealthKit
|
|
253
|
+
import Synchronization
|
|
273
254
|
|
|
274
|
-
|
|
255
|
+
final class GlucoseFeed: Sendable {
|
|
256
|
+
private let store: HKHealthStore
|
|
257
|
+
private let anchor = Mutex<HKQueryAnchor?>(nil)
|
|
275
258
|
|
|
276
|
-
|
|
277
|
-
let stepType = HKQuantityType(.stepCount)
|
|
259
|
+
init(store: HKHealthStore) { self.store = store }
|
|
278
260
|
|
|
279
|
-
|
|
280
|
-
let
|
|
281
|
-
type:
|
|
261
|
+
func start(onChange: @escaping @Sendable ([HKSample], [HKDeletedObject]) -> Void) {
|
|
262
|
+
let query = HKAnchoredObjectQuery(
|
|
263
|
+
type: HKQuantityType(.bloodGlucose),
|
|
282
264
|
predicate: nil,
|
|
283
|
-
anchor: anchor,
|
|
265
|
+
anchor: anchor.withLock { $0 },
|
|
284
266
|
limit: HKObjectQueryNoLimit
|
|
285
|
-
) { [weak self]
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
self.processNewSamples(added ?? [])
|
|
267
|
+
) { [weak self] _, added, removed, nextAnchor, _ in
|
|
268
|
+
self?.anchor.withLock { $0 = nextAnchor }
|
|
269
|
+
onChange(added ?? [], removed ?? [])
|
|
289
270
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
guard let self else { return }
|
|
294
|
-
self.anchor = newAnchor
|
|
295
|
-
self.processNewSamples(added ?? [])
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
healthStore.execute(anchoredQuery)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private func processNewSamples(_ samples: [HKSample]) {
|
|
302
|
-
for sample in samples {
|
|
303
|
-
guard let quantitySample = sample as? HKQuantitySample else { continue }
|
|
304
|
-
let steps = quantitySample.quantity.doubleValue(for: .count())
|
|
305
|
-
totalSteps += steps
|
|
271
|
+
query.updateHandler = { [weak self] _, added, removed, nextAnchor, _ in
|
|
272
|
+
self?.anchor.withLock { $0 = nextAnchor }
|
|
273
|
+
onChange(added ?? [], removed ?? [])
|
|
306
274
|
}
|
|
275
|
+
store.execute(query)
|
|
307
276
|
}
|
|
308
277
|
}
|
|
309
|
-
```
|
|
310
278
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
let
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
anchor: savedAnchor
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
// One-shot
|
|
323
|
-
let result = try await descriptor.result(for: healthStore)
|
|
324
|
-
let newSamples = result.addedSamples
|
|
325
|
-
let deletedObjects = result.deletedObjects
|
|
326
|
-
let newAnchor = result.newAnchor
|
|
327
|
-
|
|
328
|
-
// Long-running with updates
|
|
329
|
-
for try await result in descriptor.results(for: healthStore) {
|
|
330
|
-
// Process result.addedSamples and result.deletedObjects
|
|
279
|
+
func glucoseChanges(since mark: HKQueryAnchor?, store: HKHealthStore) async throws -> HKQueryAnchor {
|
|
280
|
+
let query = HKAnchoredObjectQueryDescriptor(
|
|
281
|
+
predicates: [.quantitySample(type: HKQuantityType(.bloodGlucose))],
|
|
282
|
+
anchor: mark
|
|
283
|
+
)
|
|
284
|
+
let delta = try await query.result(for: store)
|
|
285
|
+
_ = (delta.addedSamples, delta.deletedObjects)
|
|
286
|
+
return delta.newAnchor
|
|
331
287
|
}
|
|
332
288
|
```
|
|
333
289
|
|
|
334
|
-
|
|
290
|
+
The first handler delivers the initial batch; `updateHandler` keeps delivering
|
|
291
|
+
changes. Both run on a background queue, so the saved anchor sits behind a
|
|
292
|
+
`Mutex`. With the descriptor, `result(for:)` returns `addedSamples`,
|
|
293
|
+
`deletedObjects` and `newAnchor`, and `results(for:)` streams further updates.
|
|
335
294
|
|
|
336
|
-
|
|
295
|
+
## Predicates
|
|
337
296
|
|
|
338
297
|
```swift
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)!
|
|
342
|
-
let todayPredicate = HKQuery.predicateForSamples(
|
|
343
|
-
withStart: today, end: tomorrow
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
// Samples from the last 7 days
|
|
347
|
-
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
|
|
348
|
-
let weekPredicate = HKQuery.predicateForSamples(
|
|
349
|
-
withStart: oneWeekAgo, end: Date()
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
// Strict: sample must be entirely within the range
|
|
353
|
-
let strictPredicate = HKQuery.predicateForSamples(
|
|
354
|
-
withStart: today, end: tomorrow,
|
|
355
|
-
options: .strictStartDate
|
|
356
|
-
)
|
|
357
|
-
```
|
|
298
|
+
import Foundation
|
|
299
|
+
import HealthKit
|
|
358
300
|
|
|
359
|
-
|
|
301
|
+
func predicates() -> NSPredicate? {
|
|
302
|
+
let now = Date()
|
|
303
|
+
let calendar = Calendar.current
|
|
304
|
+
let todayStart = calendar.startOfDay(for: now)
|
|
305
|
+
guard let weekAgo = calendar.date(byAdding: .day, value: -7, to: now) else { return nil }
|
|
360
306
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
let
|
|
364
|
-
from: HKSource.default()
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
// Only samples from Apple Watch
|
|
368
|
-
let devicePredicate = HKQuery.predicateForObjects(
|
|
369
|
-
withDeviceProperty: HKDevicePropertyKeyModel,
|
|
370
|
-
allowedValues: ["Watch"]
|
|
371
|
-
)
|
|
372
|
-
```
|
|
307
|
+
let today = HKQuery.predicateForSamples(withStart: todayStart, end: now)
|
|
308
|
+
let lastWeek = HKQuery.predicateForSamples(withStart: weekAgo, end: now)
|
|
309
|
+
let startsInWeek = HKQuery.predicateForSamples(withStart: weekAgo, end: now, options: .strictStartDate)
|
|
310
|
+
let mine = HKQuery.predicateForObjects(from: HKSource.default())
|
|
311
|
+
let fromWatch = HKQuery.predicateForObjects(withDeviceProperty: HKDevicePropertyKeyModel, allowedValues: ["Watch"])
|
|
373
312
|
|
|
374
|
-
|
|
313
|
+
_ = (today, lastWeek)
|
|
314
|
+
return NSCompoundPredicate(andPredicateWithSubpredicates: [startsInWeek, mine, fromWatch])
|
|
315
|
+
}
|
|
375
316
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
predicates: [.quantitySample(type: stepType, predicate: todayFromWatch)],
|
|
383
|
-
sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)]
|
|
384
|
-
)
|
|
317
|
+
func watchStepsThisWeek(_ filter: NSPredicate) -> HKSampleQueryDescriptor<HKQuantitySample> {
|
|
318
|
+
HKSampleQueryDescriptor(
|
|
319
|
+
predicates: [.quantitySample(type: HKQuantityType(.stepCount), predicate: filter)],
|
|
320
|
+
sortDescriptors: []
|
|
321
|
+
)
|
|
322
|
+
}
|
|
385
323
|
```
|
|
386
324
|
|
|
387
|
-
|
|
325
|
+
- `predicateForSamples(withStart:end:)` filters by time range.
|
|
326
|
+
- Adding `options: .strictStartDate` keeps only samples whose start date falls
|
|
327
|
+
inside the range.
|
|
328
|
+
- `predicateForObjects(from: HKSource.default())` keeps this app's own samples.
|
|
329
|
+
- `predicateForObjects(withDeviceProperty:allowedValues:)` filters by device,
|
|
330
|
+
here the model `"Watch"`.
|
|
331
|
+
- `NSCompoundPredicate(andPredicateWithSubpredicates:)` combines them before
|
|
332
|
+
they go into `.quantitySample(type:predicate:)`.
|
|
388
333
|
|
|
389
|
-
|
|
334
|
+
## Chart data
|
|
390
335
|
|
|
391
336
|
```swift
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
let
|
|
337
|
+
import HealthKit
|
|
338
|
+
|
|
339
|
+
struct DayTotal: Identifiable {
|
|
340
|
+
let day: Date
|
|
341
|
+
let value: Double
|
|
342
|
+
var id: Date { day }
|
|
396
343
|
}
|
|
397
344
|
|
|
398
|
-
func
|
|
345
|
+
func lastSevenDaysOfSteps(_ store: HKHealthStore) async throws -> [DayTotal] {
|
|
399
346
|
let calendar = Calendar.current
|
|
400
|
-
let
|
|
401
|
-
|
|
402
|
-
let startDate = calendar.date(byAdding: .day, value: -7, to: endDate)!
|
|
403
|
-
|
|
404
|
-
let stepType = HKQuantityType(.stepCount)
|
|
405
|
-
let predicate = HKQuery.predicateForSamples(
|
|
406
|
-
withStart: startDate, end: endDate
|
|
407
|
-
)
|
|
408
|
-
let samplePredicate = HKSamplePredicate.quantitySample(
|
|
409
|
-
type: stepType, predicate: predicate
|
|
410
|
-
)
|
|
411
|
-
|
|
347
|
+
guard let end = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: .now)),
|
|
348
|
+
let start = calendar.date(byAdding: .day, value: -7, to: end) else { return [] }
|
|
412
349
|
let query = HKStatisticsCollectionQueryDescriptor(
|
|
413
|
-
predicate:
|
|
350
|
+
predicate: .quantitySample(type: HKQuantityType(.stepCount),
|
|
351
|
+
predicate: HKQuery.predicateForSamples(withStart: start, end: end)),
|
|
414
352
|
options: .cumulativeSum,
|
|
415
|
-
anchorDate:
|
|
353
|
+
anchorDate: end,
|
|
416
354
|
intervalComponents: DateComponents(day: 1)
|
|
417
355
|
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
var data: [DailyStepData] = []
|
|
422
|
-
collection.statisticsCollection.enumerateStatistics(
|
|
423
|
-
from: startDate, to: endDate
|
|
424
|
-
) { statistics, _ in
|
|
425
|
-
let steps = statistics.sumQuantity()?
|
|
426
|
-
.doubleValue(for: .count()) ?? 0
|
|
427
|
-
data.append(DailyStepData(date: statistics.startDate, steps: steps))
|
|
356
|
+
var totals: [DayTotal] = []
|
|
357
|
+
try await query.result(for: store).enumerateStatistics(from: start, to: end) { stats, _ in
|
|
358
|
+
totals.append(DayTotal(day: stats.startDate, value: stats.sumQuantity()?.doubleValue(for: .count()) ?? 0))
|
|
428
359
|
}
|
|
429
|
-
|
|
430
|
-
return data
|
|
360
|
+
return totals
|
|
431
361
|
}
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
### Hourly Heart Rate Averages
|
|
435
|
-
|
|
436
|
-
```swift
|
|
437
|
-
func fetchHourlyHeartRate(for date: Date) async throws -> [(hour: Date, bpm: Double)] {
|
|
438
|
-
let calendar = Calendar.current
|
|
439
|
-
let startOfDay = calendar.startOfDay(for: date)
|
|
440
|
-
let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!
|
|
441
|
-
|
|
442
|
-
let heartRateType = HKQuantityType(.heartRate)
|
|
443
|
-
let predicate = HKQuery.predicateForSamples(
|
|
444
|
-
withStart: startOfDay, end: endOfDay
|
|
445
|
-
)
|
|
446
|
-
let samplePredicate = HKSamplePredicate.quantitySample(
|
|
447
|
-
type: heartRateType, predicate: predicate
|
|
448
|
-
)
|
|
449
362
|
|
|
363
|
+
func hourlyPulse(_ store: HKHealthStore, on day: Date) async throws -> [DayTotal] {
|
|
364
|
+
let start = Calendar.current.startOfDay(for: day)
|
|
365
|
+
let end = start.addingTimeInterval(24 * 3_600)
|
|
450
366
|
let query = HKStatisticsCollectionQueryDescriptor(
|
|
451
|
-
predicate:
|
|
367
|
+
predicate: .quantitySample(type: HKQuantityType(.heartRate),
|
|
368
|
+
predicate: HKQuery.predicateForSamples(withStart: start, end: end)),
|
|
452
369
|
options: .discreteAverage,
|
|
453
|
-
anchorDate:
|
|
370
|
+
anchorDate: start,
|
|
454
371
|
intervalComponents: DateComponents(hour: 1)
|
|
455
372
|
)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
collection.statisticsCollection.enumerateStatistics(
|
|
462
|
-
from: startOfDay, to: endOfDay
|
|
463
|
-
) { statistics, _ in
|
|
464
|
-
if let avg = statistics.averageQuantity()?.doubleValue(for: unit) {
|
|
465
|
-
hourlyData.append((hour: statistics.startDate, bpm: avg))
|
|
373
|
+
let bpm = HKUnit.count().unitDivided(by: .minute())
|
|
374
|
+
var points: [DayTotal] = []
|
|
375
|
+
try await query.result(for: store).enumerateStatistics(from: start, to: end) { stats, _ in
|
|
376
|
+
if let average = stats.averageQuantity()?.doubleValue(for: bpm) {
|
|
377
|
+
points.append(DayTotal(day: stats.startDate, value: average))
|
|
466
378
|
}
|
|
467
379
|
}
|
|
468
|
-
|
|
469
|
-
return hourlyData
|
|
380
|
+
return points
|
|
470
381
|
}
|
|
471
382
|
```
|
|
472
383
|
|
|
473
|
-
|
|
384
|
+
The weekly chart covers the seven days ending at the start of tomorrow and
|
|
385
|
+
anchors on that end date. The hourly chart uses `.discreteAverage` and adds a
|
|
386
|
+
point only for hours that have data.
|
|
474
387
|
|
|
475
|
-
|
|
388
|
+
## SwiftUI integration
|
|
476
389
|
|
|
477
390
|
```swift
|
|
478
391
|
import HealthKit
|
|
392
|
+
import Observation
|
|
479
393
|
import SwiftUI
|
|
480
394
|
|
|
481
395
|
@Observable
|
|
482
396
|
@MainActor
|
|
483
|
-
final class
|
|
484
|
-
let
|
|
485
|
-
|
|
486
|
-
var isAuthorized = false
|
|
487
|
-
var
|
|
488
|
-
var
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
try await healthStore.requestAuthorization(
|
|
501
|
-
toShare: [],
|
|
502
|
-
read: typesToRead
|
|
503
|
-
)
|
|
504
|
-
isAuthorized = true
|
|
397
|
+
final class VitalsModel {
|
|
398
|
+
private let store = HKHealthStore()
|
|
399
|
+
var isAvailable: Bool { HKHealthStore.isHealthDataAvailable() }
|
|
400
|
+
private(set) var isAuthorized = false
|
|
401
|
+
private(set) var steps: Double = 0
|
|
402
|
+
private(set) var restingPulse: Double?
|
|
403
|
+
|
|
404
|
+
func authorize() async {
|
|
405
|
+
do {
|
|
406
|
+
try await store.requestAuthorization(toShare: [], read: [
|
|
407
|
+
HKQuantityType(.stepCount), HKQuantityType(.restingHeartRate)
|
|
408
|
+
])
|
|
409
|
+
isAuthorized = true
|
|
410
|
+
} catch {
|
|
411
|
+
isAuthorized = false
|
|
412
|
+
}
|
|
505
413
|
}
|
|
506
414
|
|
|
507
|
-
func
|
|
508
|
-
async let
|
|
509
|
-
async let
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
recentHeartRate = (try? await heartRate) ?? 0
|
|
415
|
+
func refresh() async {
|
|
416
|
+
async let stepTotal = todaysSteps()
|
|
417
|
+
async let pulse = latestRestingPulse()
|
|
418
|
+
steps = (try? await stepTotal) ?? 0
|
|
419
|
+
restingPulse = try? await pulse
|
|
513
420
|
}
|
|
514
421
|
|
|
515
|
-
private func
|
|
516
|
-
let
|
|
517
|
-
let startOfDay = calendar.startOfDay(for: Date())
|
|
518
|
-
let predicate = HKQuery.predicateForSamples(
|
|
519
|
-
withStart: startOfDay, end: Date()
|
|
520
|
-
)
|
|
521
|
-
let stepType = HKQuantityType(.stepCount)
|
|
522
|
-
let samplePredicate = HKSamplePredicate.quantitySample(
|
|
523
|
-
type: stepType, predicate: predicate
|
|
524
|
-
)
|
|
422
|
+
private func todaysSteps() async throws -> Double {
|
|
423
|
+
let start = Calendar.current.startOfDay(for: .now)
|
|
525
424
|
let query = HKStatisticsQueryDescriptor(
|
|
526
|
-
predicate:
|
|
425
|
+
predicate: .quantitySample(type: HKQuantityType(.stepCount),
|
|
426
|
+
predicate: HKQuery.predicateForSamples(withStart: start, end: .now)),
|
|
427
|
+
options: .cumulativeSum
|
|
527
428
|
)
|
|
528
|
-
return try await query.result(for:
|
|
529
|
-
.sumQuantity()?.doubleValue(for: .count()) ?? 0
|
|
429
|
+
return try await query.result(for: store)?.sumQuantity()?.doubleValue(for: .count()) ?? 0
|
|
530
430
|
}
|
|
531
431
|
|
|
532
|
-
private func
|
|
533
|
-
let
|
|
534
|
-
|
|
535
|
-
predicates: [.quantitySample(type: heartRateType)],
|
|
432
|
+
private func latestRestingPulse() async throws -> Double? {
|
|
433
|
+
let query = HKSampleQueryDescriptor(
|
|
434
|
+
predicates: [.quantitySample(type: HKQuantityType(.restingHeartRate))],
|
|
536
435
|
sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)],
|
|
537
436
|
limit: 1
|
|
538
437
|
)
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
return results.first?.quantity.doubleValue(for: unit) ?? 0
|
|
438
|
+
return try await query.result(for: store).first?.quantity
|
|
439
|
+
.doubleValue(for: .count().unitDivided(by: .minute()))
|
|
542
440
|
}
|
|
543
441
|
}
|
|
544
|
-
```
|
|
545
442
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
```swift
|
|
549
|
-
struct HealthDashboardView: View {
|
|
550
|
-
@Environment(HealthManager.self) private var healthManager
|
|
443
|
+
struct VitalsScreen: View {
|
|
444
|
+
@Environment(VitalsModel.self) private var model
|
|
551
445
|
|
|
552
446
|
var body: some View {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
)
|
|
561
|
-
} else if !healthManager.isAuthorized {
|
|
562
|
-
authorizationPrompt
|
|
563
|
-
} else {
|
|
564
|
-
healthDataView
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
.navigationTitle("Health")
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
private var authorizationPrompt: some View {
|
|
572
|
-
ContentUnavailableView {
|
|
573
|
-
Label("Health Access", systemImage: "heart.text.square")
|
|
574
|
-
} description: {
|
|
575
|
-
Text("Grant access to view your health data.")
|
|
576
|
-
} actions: {
|
|
577
|
-
Button("Authorize") {
|
|
578
|
-
Task {
|
|
579
|
-
try? await healthManager.requestAuthorization()
|
|
580
|
-
}
|
|
447
|
+
if !model.isAvailable {
|
|
448
|
+
ContentUnavailableView("Health data unavailable", systemImage: "heart.slash")
|
|
449
|
+
} else if !model.isAuthorized {
|
|
450
|
+
ContentUnavailableView {
|
|
451
|
+
Label("Connect to Health", systemImage: "heart.text.square")
|
|
452
|
+
} actions: {
|
|
453
|
+
Button("Allow access") { Task { await model.authorize() } }
|
|
581
454
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
private var healthDataView: some View {
|
|
587
|
-
List {
|
|
588
|
-
Section("Today") {
|
|
589
|
-
LabeledContent("Steps") {
|
|
590
|
-
Text(healthManager.todaySteps, format: .number.precision(.fractionLength(0)))
|
|
591
|
-
}
|
|
592
|
-
LabeledContent("Heart Rate") {
|
|
593
|
-
Text("\(Int(healthManager.recentHeartRate)) bpm")
|
|
594
|
-
}
|
|
455
|
+
} else {
|
|
456
|
+
List {
|
|
457
|
+
LabeledContent("Steps today", value: model.steps, format: .number)
|
|
458
|
+
LabeledContent("Resting pulse", value: model.restingPulse.map { "\(Int($0)) bpm" } ?? "-")
|
|
595
459
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
await healthManager.refreshData()
|
|
599
|
-
}
|
|
600
|
-
.refreshable {
|
|
601
|
-
await healthManager.refreshData()
|
|
460
|
+
.task { await model.refresh() }
|
|
461
|
+
.refreshable { await model.refresh() }
|
|
602
462
|
}
|
|
603
463
|
}
|
|
604
464
|
}
|
|
605
|
-
```
|
|
606
465
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
```swift
|
|
610
|
-
@main
|
|
611
|
-
struct MyHealthApp: App {
|
|
612
|
-
@State private var healthManager = HealthManager()
|
|
466
|
+
struct VitalsApp: App {
|
|
467
|
+
@State private var model = VitalsModel()
|
|
613
468
|
|
|
614
469
|
var body: some Scene {
|
|
615
470
|
WindowGroup {
|
|
616
|
-
|
|
617
|
-
.environment(healthManager)
|
|
471
|
+
VitalsScreen().environment(model)
|
|
618
472
|
}
|
|
619
473
|
}
|
|
620
474
|
}
|
|
621
475
|
```
|
|
622
476
|
|
|
623
|
-
|
|
477
|
+
- The model requests read-only access by passing an empty `toShare` set.
|
|
478
|
+
- `refresh()` runs both fetches at once with `async let`.
|
|
479
|
+
- The latest value comes from a sample descriptor with `limit: 1` sorted by end
|
|
480
|
+
date, newest first.
|
|
481
|
+
- The screen checks availability first, then authorization, then shows the
|
|
482
|
+
data. `isAuthorized` becomes `true` once `requestAuthorization` returns
|
|
483
|
+
without throwing, which only means the sheet was handled, not that reads were
|
|
484
|
+
allowed.
|
|
485
|
+
- The app creates the model as `@State` and injects it with `.environment`.
|
|
624
486
|
|
|
625
|
-
|
|
487
|
+
## Characteristics
|
|
488
|
+
|
|
489
|
+
Characteristics are values the user enters once in Health, such as blood type.
|
|
490
|
+
They are read directly; no query is involved, and all of these calls throw.
|
|
626
491
|
|
|
627
492
|
```swift
|
|
628
|
-
|
|
629
|
-
// Date of birth
|
|
630
|
-
let dobComponents = try healthStore.dateOfBirthComponents()
|
|
631
|
-
let calendar = Calendar.current
|
|
632
|
-
if let dob = calendar.date(from: dobComponents) {
|
|
633
|
-
let age = calendar.dateComponents([.year], from: dob, to: Date()).year ?? 0
|
|
634
|
-
print("Age: \(age)")
|
|
635
|
-
}
|
|
493
|
+
import HealthKit
|
|
636
494
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
@unknown default: break
|
|
645
|
-
}
|
|
495
|
+
struct Profile {
|
|
496
|
+
var age: Int?
|
|
497
|
+
var sex: HKBiologicalSex = .notSet
|
|
498
|
+
var blood: HKBloodType = .notSet
|
|
499
|
+
var skin: HKFitzpatrickSkinType = .notSet
|
|
500
|
+
var wheelchair: HKWheelchairUse = .notSet
|
|
501
|
+
}
|
|
646
502
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
case .notSet: print("Not set")
|
|
659
|
-
@unknown default: break
|
|
503
|
+
func loadProfile(_ store: HKHealthStore) -> Profile {
|
|
504
|
+
var profile = Profile()
|
|
505
|
+
do {
|
|
506
|
+
let born = try store.dateOfBirthComponents()
|
|
507
|
+
if let birthday = Calendar.current.date(from: born) {
|
|
508
|
+
profile.age = Calendar.current.dateComponents([.year], from: birthday, to: .now).year
|
|
509
|
+
}
|
|
510
|
+
} catch let error as HKError where error.code == .errorNoData {
|
|
511
|
+
profile.age = nil
|
|
512
|
+
} catch {
|
|
513
|
+
profile.age = nil
|
|
660
514
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
// Wheelchair use
|
|
667
|
-
let wheelchair = try healthStore.wheelchairUse().wheelchairUse
|
|
668
|
-
print("Wheelchair: \(wheelchair == .yes)")
|
|
515
|
+
profile.sex = (try? store.biologicalSex().biologicalSex) ?? .notSet
|
|
516
|
+
profile.blood = (try? store.bloodType().bloodType) ?? .notSet
|
|
517
|
+
profile.skin = (try? store.fitzpatrickSkinType().skinType) ?? .notSet
|
|
518
|
+
profile.wheelchair = (try? store.wheelchairUse().wheelchairUse) ?? .notSet
|
|
519
|
+
return profile
|
|
669
520
|
}
|
|
670
521
|
```
|
|
671
522
|
|
|
672
|
-
|
|
523
|
+
| Type | Cases |
|
|
524
|
+
|---|---|
|
|
525
|
+
| `HKBiologicalSex` | `.female`, `.male`, `.other`, `.notSet` |
|
|
526
|
+
| `HKBloodType` | `.aPositive`, `.aNegative`, `.bPositive`, `.bNegative`, `.abPositive`, `.abNegative`, `.oPositive`, `.oNegative`, `.notSet` |
|
|
673
527
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
let dob = try healthStore.dateOfBirthComponents()
|
|
677
|
-
// Use dob
|
|
678
|
-
} catch let error as HKError where error.code == .errorNoData {
|
|
679
|
-
// User hasn't set date of birth in Health app
|
|
680
|
-
} catch {
|
|
681
|
-
// Other error
|
|
682
|
-
}
|
|
683
|
-
```
|
|
528
|
+
An unset value throws `HKError` with code `.errorNoData`; catch that case
|
|
529
|
+
separately from real failures.
|