@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,141 +1,117 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ActivityKit Patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Worked patterns for Live Activities. Everything here uses async/await and
|
|
4
|
+
`ActivityContent`, so it needs iOS 16.2 or later unless a heading names a
|
|
5
|
+
newer release.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
9
|
-
- [
|
|
10
|
-
- [Starting
|
|
11
|
-
- [Updating from the
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
- [A complete attributes model](#a-complete-attributes-model)
|
|
10
|
+
- [Starting, with every parameter](#starting-with-every-parameter)
|
|
11
|
+
- [Updating from the app](#updating-from-the-app)
|
|
12
|
+
- [Server payloads](#server-payloads)
|
|
13
|
+
- [Channel pushes (iOS 18+)](#channel-pushes-ios-18)
|
|
14
|
+
- [Ending by outcome](#ending-by-outcome)
|
|
15
|
+
- [Dynamic Island, all regions](#dynamic-island-all-regions)
|
|
16
|
+
- [Lock Screen view with timer and progress](#lock-screen-view-with-timer-and-progress)
|
|
17
|
+
- [Several activities at once](#several-activities-at-once)
|
|
18
|
+
- [Observing state and content](#observing-state-and-content)
|
|
19
|
+
- [Tokens](#tokens)
|
|
20
|
+
- [Authorization](#authorization)
|
|
21
|
+
- [Errors](#errors)
|
|
22
|
+
- [Background behaviour](#background-behaviour)
|
|
23
|
+
- [Testing](#testing)
|
|
24
|
+
- [Info.plist keys](#infoplist-keys)
|
|
25
|
+
- [Apple documentation](#apple-documentation)
|
|
26
|
+
|
|
27
|
+
## A complete attributes model
|
|
28
|
+
|
|
29
|
+
A field-service visit: a technician drives to the customer and does the job.
|
|
30
30
|
|
|
31
31
|
```swift
|
|
32
32
|
import ActivityKit
|
|
33
33
|
|
|
34
|
-
struct
|
|
35
|
-
// Static -- set at creation, immutable for the activity lifetime
|
|
36
|
-
var riderName: String
|
|
37
|
-
var pickupLocation: String
|
|
38
|
-
var dropoffLocation: String
|
|
39
|
-
|
|
34
|
+
struct ServiceVisitAttributes: ActivityAttributes {
|
|
40
35
|
struct ContentState: Codable, Hashable {
|
|
41
|
-
var
|
|
42
|
-
var
|
|
43
|
-
var
|
|
44
|
-
var
|
|
45
|
-
var
|
|
46
|
-
|
|
47
|
-
var
|
|
48
|
-
var distanceRemaining: Double // miles
|
|
36
|
+
var technicianName: String
|
|
37
|
+
var avatarSymbol: String // SF Symbol or asset catalog name
|
|
38
|
+
var vanDescription: String
|
|
39
|
+
var arrivalStartEpoch: Int // seconds since 1970
|
|
40
|
+
var arrivalEndEpoch: Int
|
|
41
|
+
var status: VisitStatus
|
|
42
|
+
var kilometresAway: Double
|
|
49
43
|
}
|
|
50
|
-
}
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
case driverArrived
|
|
56
|
-
case inProgress
|
|
57
|
-
case arriving
|
|
58
|
-
case completed
|
|
59
|
-
case cancelled
|
|
60
|
-
case failed
|
|
45
|
+
var customerFirstName: String
|
|
46
|
+
var serviceAddress: String
|
|
47
|
+
var jobSummary: String
|
|
61
48
|
}
|
|
62
49
|
|
|
63
|
-
extension
|
|
64
|
-
var
|
|
65
|
-
Date(timeIntervalSince1970: TimeInterval(
|
|
66
|
-
|
|
50
|
+
extension ServiceVisitAttributes.ContentState {
|
|
51
|
+
var arrivalWindow: ClosedRange<Date> {
|
|
52
|
+
Date(timeIntervalSince1970: TimeInterval(arrivalStartEpoch))
|
|
53
|
+
...
|
|
54
|
+
Date(timeIntervalSince1970: TimeInterval(arrivalEndEpoch))
|
|
67
55
|
}
|
|
68
56
|
}
|
|
57
|
+
|
|
58
|
+
enum VisitStatus: String, Codable, Hashable {
|
|
59
|
+
case scheduled, enRoute, nearby, onSite, finishing, completed, cancelled, failed
|
|
60
|
+
}
|
|
69
61
|
```
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
Why the epoch integers: a server has to produce `content-state` JSON that
|
|
64
|
+
decodes into this type. Plain scalars have one obvious encoding, so the Swift
|
|
65
|
+
side and the server cannot drift. The view-friendly `ClosedRange<Date>` is
|
|
66
|
+
derived in an extension. The alternative is a custom `Codable` implementation
|
|
67
|
+
whose keys and value formats are written down and agreed with the server.
|
|
74
68
|
|
|
75
|
-
|
|
69
|
+
Size budget: the attributes and the content state together must fit in the
|
|
70
|
+
4 KB ActivityKit allows. Leave images, long strings and deep nesting out of
|
|
71
|
+
`ContentState`.
|
|
72
|
+
|
|
73
|
+
## Starting, with every parameter
|
|
74
|
+
|
|
75
|
+
The token loops below use a small helper:
|
|
76
76
|
|
|
77
77
|
```swift
|
|
78
|
-
|
|
78
|
+
extension Data {
|
|
79
|
+
var hexEncoded: String { reduce(into: "") { $0 += String(format: "%02x", $1) } }
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```swift
|
|
84
|
+
enum VisitActivityError: Error { case liveActivitiesOff }
|
|
79
85
|
|
|
80
86
|
@MainActor
|
|
81
|
-
func
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
) async throws -> Activity<RideAttributes> {
|
|
88
|
-
// Check authorization before attempting to start
|
|
89
|
-
let authInfo = ActivityAuthorizationInfo()
|
|
90
|
-
guard authInfo.areActivitiesEnabled else {
|
|
91
|
-
throw RideError.liveActivitiesDisabled
|
|
87
|
+
func startVisitActivity(
|
|
88
|
+
attributes: ServiceVisitAttributes,
|
|
89
|
+
initial: ServiceVisitAttributes.ContentState
|
|
90
|
+
) async throws -> Activity<ServiceVisitAttributes> {
|
|
91
|
+
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
|
|
92
|
+
throw VisitActivityError.liveActivitiesOff
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
let
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
dropoffLocation: dropoff
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
let initialState = RideAttributes.ContentState(
|
|
101
|
-
driverName: driver,
|
|
102
|
-
driverPhoto: "car.fill",
|
|
103
|
-
vehicleDescription: vehicle,
|
|
104
|
-
etaStartSeconds: Int(Date().timeIntervalSince1970),
|
|
105
|
-
etaEndSeconds: Int(Date().addingTimeInterval(600).timeIntervalSince1970),
|
|
106
|
-
status: .driverAssigned,
|
|
107
|
-
distanceRemaining: 2.5
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
let content = ActivityContent(
|
|
111
|
-
state: initialState,
|
|
112
|
-
staleDate: Date().addingTimeInterval(120), // stale after 2 min
|
|
95
|
+
let opening = ActivityContent(
|
|
96
|
+
state: initial,
|
|
97
|
+
staleDate: .now.addingTimeInterval(120),
|
|
113
98
|
relevanceScore: 80
|
|
114
99
|
)
|
|
115
|
-
|
|
116
100
|
let activity = try Activity.request(
|
|
117
|
-
attributes: attributes,
|
|
118
|
-
content: content,
|
|
119
|
-
pushType: .token // Enable push updates
|
|
101
|
+
attributes: attributes, content: opening, pushType: .token
|
|
120
102
|
)
|
|
121
103
|
|
|
122
|
-
// Forward push token to server for remote updates
|
|
123
104
|
Task {
|
|
124
|
-
for await
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
tokenString, rideID: activity.id
|
|
105
|
+
for await tokenData in activity.pushTokenUpdates {
|
|
106
|
+
try? await VisitBackend.shared.register(
|
|
107
|
+
updateToken: tokenData.hexEncoded, activityID: activity.id
|
|
128
108
|
)
|
|
129
109
|
}
|
|
130
110
|
}
|
|
131
111
|
|
|
132
|
-
// Observe state changes for cleanup
|
|
133
112
|
Task {
|
|
134
|
-
for await
|
|
135
|
-
|
|
136
|
-
// Activity removed from UI -- clean up local resources
|
|
137
|
-
RideStore.shared.removeActivity(id: activity.id)
|
|
138
|
-
}
|
|
113
|
+
for await lifecycle in activity.activityStateUpdates where lifecycle == .dismissed {
|
|
114
|
+
VisitCache.shared.purge(activityID: activity.id)
|
|
139
115
|
}
|
|
140
116
|
}
|
|
141
117
|
|
|
@@ -143,811 +119,603 @@ func startRideActivity(
|
|
|
143
119
|
}
|
|
144
120
|
```
|
|
145
121
|
|
|
146
|
-
###
|
|
147
|
-
|
|
148
|
-
Schedule the activity to appear at a future time without the app in foreground:
|
|
122
|
+
### Scheduled start (iOS 26+)
|
|
149
123
|
|
|
150
124
|
```swift
|
|
151
|
-
let gameTime = Calendar.current.date(
|
|
152
|
-
from: DateComponents(year: 2026, month: 3, day: 15, hour: 19, minute: 0)
|
|
153
|
-
)!
|
|
154
|
-
|
|
155
125
|
let activity = try Activity.request(
|
|
156
|
-
attributes:
|
|
157
|
-
content:
|
|
126
|
+
attributes: visit,
|
|
127
|
+
content: opening,
|
|
158
128
|
pushType: .token,
|
|
159
129
|
style: .standard,
|
|
160
130
|
alertConfiguration: AlertConfiguration(
|
|
161
|
-
title: "
|
|
162
|
-
body: "
|
|
131
|
+
title: "Technician on the way",
|
|
132
|
+
body: "Your visit window has started.",
|
|
163
133
|
sound: .default
|
|
164
134
|
),
|
|
165
|
-
start:
|
|
135
|
+
start: visitWindowOpens
|
|
166
136
|
)
|
|
167
137
|
```
|
|
168
138
|
|
|
169
|
-
###
|
|
170
|
-
|
|
171
|
-
Use `.standard` for persistent Live Activities that should remain visible until
|
|
172
|
-
the app, push, user, or system duration limit ends them. `.transient` is only
|
|
173
|
-
for short-lived expanded Dynamic Island presentations that can auto-end when the
|
|
174
|
-
user locks the device, collapses or shrinks the expanded presentation, leaves
|
|
175
|
-
the app, or does other work outside Dynamic Island; it is wrong for persistent
|
|
176
|
-
Live Activities.
|
|
139
|
+
### Choosing a style (iOS 18+)
|
|
177
140
|
|
|
178
141
|
```swift
|
|
179
|
-
let
|
|
180
|
-
attributes:
|
|
181
|
-
content: content,
|
|
182
|
-
pushType: .token,
|
|
183
|
-
style: .standard
|
|
142
|
+
let persistent = try Activity.request(
|
|
143
|
+
attributes: visit, content: opening, pushType: .token, style: .standard
|
|
184
144
|
)
|
|
185
145
|
```
|
|
186
146
|
|
|
187
|
-
|
|
147
|
+
A `.standard` activity stays until the app, a push, the user or the system
|
|
148
|
+
duration limit ends it. `.transient` is the wrong choice for anything meant to
|
|
149
|
+
persist; reserve it for brief expanded Dynamic Island moments.
|
|
150
|
+
|
|
151
|
+
## Updating from the app
|
|
152
|
+
|
|
153
|
+
Copy the fields that did not change from the current state, and convert dates
|
|
154
|
+
back to the agreed scalar form.
|
|
188
155
|
|
|
189
156
|
```swift
|
|
190
|
-
func
|
|
191
|
-
_
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
157
|
+
func applyStatus(
|
|
158
|
+
_ status: VisitStatus,
|
|
159
|
+
newWindow: ClosedRange<Date>,
|
|
160
|
+
kilometres: Double,
|
|
161
|
+
to activity: Activity<ServiceVisitAttributes>,
|
|
162
|
+
notify: Bool
|
|
196
163
|
) async {
|
|
197
|
-
let
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
164
|
+
let current = activity.content.state
|
|
165
|
+
let state = ServiceVisitAttributes.ContentState(
|
|
166
|
+
technicianName: current.technicianName,
|
|
167
|
+
avatarSymbol: current.avatarSymbol,
|
|
168
|
+
vanDescription: current.vanDescription,
|
|
169
|
+
arrivalStartEpoch: Int(newWindow.lowerBound.timeIntervalSince1970),
|
|
170
|
+
arrivalEndEpoch: Int(newWindow.upperBound.timeIntervalSince1970),
|
|
171
|
+
status: status,
|
|
172
|
+
kilometresAway: kilometres
|
|
205
173
|
)
|
|
206
|
-
|
|
207
174
|
let content = ActivityContent(
|
|
208
|
-
state:
|
|
209
|
-
staleDate:
|
|
210
|
-
relevanceScore:
|
|
175
|
+
state: state,
|
|
176
|
+
staleDate: .now.addingTimeInterval(120),
|
|
177
|
+
relevanceScore: status == .nearby ? 100 : 80
|
|
211
178
|
)
|
|
212
179
|
|
|
213
|
-
|
|
214
|
-
await activity.update(content, alertConfiguration: AlertConfiguration(
|
|
215
|
-
title: "Ride Update",
|
|
216
|
-
body: alertMessage(for: newStatus),
|
|
217
|
-
sound: .default
|
|
218
|
-
))
|
|
219
|
-
} else {
|
|
180
|
+
guard notify else {
|
|
220
181
|
await activity.update(content)
|
|
182
|
+
return
|
|
221
183
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
case .driverArrived: "Your driver has arrived!"
|
|
227
|
-
case .arriving: "You're almost there!"
|
|
228
|
-
case .completed: "You've arrived at your destination."
|
|
229
|
-
default: "Your ride status has changed."
|
|
184
|
+
let message: LocalizedStringResource = switch status {
|
|
185
|
+
case .nearby: "Your technician is a few minutes away."
|
|
186
|
+
case .onSite: "Your technician has arrived."
|
|
187
|
+
default: "Your visit has an update."
|
|
230
188
|
}
|
|
189
|
+
await activity.update(
|
|
190
|
+
content,
|
|
191
|
+
alertConfiguration: AlertConfiguration(title: "Service visit", body: message, sound: .default)
|
|
192
|
+
)
|
|
231
193
|
}
|
|
232
194
|
```
|
|
233
195
|
|
|
234
|
-
|
|
196
|
+
`relevanceScore` ranks this activity against the app's other activities when
|
|
197
|
+
the system has to choose what to show; raise it for the moment that matters
|
|
198
|
+
most.
|
|
199
|
+
|
|
200
|
+
## Server payloads
|
|
201
|
+
|
|
202
|
+
Keys in `content-state` are the `ContentState` property names; enums travel as
|
|
203
|
+
their raw values; numbers stay numbers.
|
|
235
204
|
|
|
236
|
-
|
|
205
|
+
Update:
|
|
237
206
|
|
|
238
207
|
```json
|
|
239
208
|
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
209
|
+
"aps": {
|
|
210
|
+
"timestamp": 1790000000,
|
|
211
|
+
"event": "update",
|
|
212
|
+
"content-state": {
|
|
213
|
+
"technicianName": "Sam",
|
|
214
|
+
"avatarSymbol": "person.crop.circle",
|
|
215
|
+
"vanDescription": "White van, blue stripe",
|
|
216
|
+
"arrivalStartEpoch": 1790000300,
|
|
217
|
+
"arrivalEndEpoch": 1790000900,
|
|
218
|
+
"status": "nearby",
|
|
219
|
+
"kilometresAway": 1.2
|
|
220
|
+
},
|
|
221
|
+
"stale-date": 1790000600,
|
|
222
|
+
"relevance-score": 100,
|
|
223
|
+
"alert": {
|
|
224
|
+
"title": "Service visit",
|
|
225
|
+
"body": "Your technician is a few minutes away.",
|
|
226
|
+
"sound": "default"
|
|
259
227
|
}
|
|
228
|
+
}
|
|
260
229
|
}
|
|
261
230
|
```
|
|
262
231
|
|
|
263
|
-
|
|
232
|
+
End:
|
|
264
233
|
|
|
265
234
|
```json
|
|
266
235
|
{
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
236
|
+
"aps": {
|
|
237
|
+
"timestamp": 1790004000,
|
|
238
|
+
"event": "end",
|
|
239
|
+
"dismissal-date": 1790007600,
|
|
240
|
+
"content-state": {
|
|
241
|
+
"technicianName": "Sam",
|
|
242
|
+
"avatarSymbol": "person.crop.circle",
|
|
243
|
+
"vanDescription": "White van, blue stripe",
|
|
244
|
+
"arrivalStartEpoch": 1790000300,
|
|
245
|
+
"arrivalEndEpoch": 1790000300,
|
|
246
|
+
"status": "completed",
|
|
247
|
+
"kilometresAway": 0
|
|
280
248
|
}
|
|
249
|
+
}
|
|
281
250
|
}
|
|
282
251
|
```
|
|
283
252
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
253
|
+
Push-to-start (iOS 17.2+), addressed to the token from `pushToStartTokenUpdates`. `attributes-type`
|
|
254
|
+
is the Swift type name, `attributes` holds the static fields, and `alert` must
|
|
255
|
+
be present:
|
|
287
256
|
|
|
288
257
|
```json
|
|
289
258
|
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
259
|
+
"aps": {
|
|
260
|
+
"timestamp": 1789990000,
|
|
261
|
+
"event": "start",
|
|
262
|
+
"attributes-type": "ServiceVisitAttributes",
|
|
263
|
+
"attributes": {
|
|
264
|
+
"customerFirstName": "Alex",
|
|
265
|
+
"serviceAddress": "12 Elm Row",
|
|
266
|
+
"jobSummary": "Boiler check"
|
|
267
|
+
},
|
|
268
|
+
"content-state": {
|
|
269
|
+
"technicianName": "Sam",
|
|
270
|
+
"avatarSymbol": "person.crop.circle",
|
|
271
|
+
"vanDescription": "White van, blue stripe",
|
|
272
|
+
"arrivalStartEpoch": 1790000300,
|
|
273
|
+
"arrivalEndEpoch": 1790000900,
|
|
274
|
+
"status": "scheduled",
|
|
275
|
+
"kilometresAway": 14.0
|
|
276
|
+
},
|
|
277
|
+
"alert": {
|
|
278
|
+
"title": "Visit booked",
|
|
279
|
+
"body": "Follow your technician live."
|
|
312
280
|
}
|
|
281
|
+
}
|
|
313
282
|
}
|
|
314
283
|
```
|
|
315
284
|
|
|
316
|
-
###
|
|
285
|
+
### HTTP headers
|
|
317
286
|
|
|
318
287
|
| Header | Value |
|
|
319
288
|
|---|---|
|
|
320
289
|
| `apns-push-type` | `liveactivity` |
|
|
321
|
-
| `apns-topic` |
|
|
322
|
-
| `apns-priority` | `5` (
|
|
323
|
-
| `authorization` | `bearer
|
|
290
|
+
| `apns-topic` | `BUNDLE_ID.push-type.liveactivity` |
|
|
291
|
+
| `apns-priority` | `5` (low) or `10` (immediate, counts against the budget) |
|
|
292
|
+
| `authorization` | `bearer JWT` with token-based auth; omit when using certificate auth |
|
|
324
293
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
`ActivityAttributes.ContentState`. Use the default synthesized `Codable` key and
|
|
328
|
-
value shape unless the Swift model declares custom `CodingKeys`; then coordinate
|
|
329
|
-
those exact keys and value shapes server-side. Do not assume `Date` or
|
|
330
|
-
`ClosedRange<Date>` values are Unix timestamp dictionaries unless your Swift
|
|
331
|
-
model explicitly encodes them that way. A type mismatch (e.g., sending a string
|
|
332
|
-
where a number is expected) can prevent ActivityKit from applying the update.
|
|
294
|
+
A type mismatch, for example `"kilometresAway": "1.2"` for a `Double`, can be
|
|
295
|
+
enough for the update to be dropped.
|
|
333
296
|
|
|
334
|
-
|
|
297
|
+
## Channel pushes (iOS 18+)
|
|
335
298
|
|
|
336
|
-
Use channel
|
|
337
|
-
|
|
338
|
-
that channel ID to the app:
|
|
299
|
+
Use a channel only with a valid channel ID that APNs created and your server
|
|
300
|
+
handed to the app:
|
|
339
301
|
|
|
340
302
|
```swift
|
|
341
|
-
let
|
|
342
|
-
attributes:
|
|
343
|
-
content:
|
|
344
|
-
pushType: .channel(
|
|
303
|
+
let broadcastVisit = try Activity.request(
|
|
304
|
+
attributes: visit,
|
|
305
|
+
content: opening,
|
|
306
|
+
pushType: .channel(channelID)
|
|
345
307
|
)
|
|
346
308
|
```
|
|
347
309
|
|
|
348
|
-
Channel pushes
|
|
349
|
-
`apns-channel-id` and expiration
|
|
350
|
-
`apns-topic` header.
|
|
310
|
+
Channel pushes update or end; they never start an activity. They are sent with
|
|
311
|
+
`apns-channel-id` and an expiration rather than the device-token `apns-topic`.
|
|
351
312
|
|
|
352
|
-
## Ending
|
|
313
|
+
## Ending by outcome
|
|
353
314
|
|
|
354
315
|
```swift
|
|
355
|
-
func
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
etaEndSeconds: Int(Date().timeIntervalSince1970),
|
|
365
|
-
status: finalStatus,
|
|
366
|
-
distanceRemaining: 0
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
let content = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)
|
|
370
|
-
|
|
371
|
-
switch finalStatus {
|
|
372
|
-
case .completed:
|
|
373
|
-
// Keep on Lock Screen for 1 hour so user can review trip details
|
|
374
|
-
await activity.end(content, dismissalPolicy: .after(
|
|
375
|
-
Date().addingTimeInterval(3600)
|
|
376
|
-
))
|
|
377
|
-
case .cancelled:
|
|
378
|
-
// Remove immediately -- no useful info to show
|
|
379
|
-
await activity.end(content, dismissalPolicy: .immediate)
|
|
380
|
-
default:
|
|
381
|
-
// Let the system decide
|
|
382
|
-
await activity.end(content, dismissalPolicy: .default)
|
|
316
|
+
func finishVisit(_ activity: Activity<ServiceVisitAttributes>, outcome: VisitStatus) async {
|
|
317
|
+
var last = activity.content.state
|
|
318
|
+
last.status = outcome
|
|
319
|
+
let final = ActivityContent(state: last, staleDate: nil, relevanceScore: 0)
|
|
320
|
+
|
|
321
|
+
let policy: ActivityUIDismissalPolicy = switch outcome {
|
|
322
|
+
case .completed: .after(.now.addingTimeInterval(3600))
|
|
323
|
+
case .cancelled: .immediate
|
|
324
|
+
default: .default
|
|
383
325
|
}
|
|
326
|
+
await activity.end(final, dismissalPolicy: policy)
|
|
384
327
|
}
|
|
385
328
|
```
|
|
386
329
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
4 additional hours, for 12 hours total from start), and app-ended `.default`
|
|
390
|
-
dismissal linger (up to 4 hours after ending).
|
|
330
|
+
A completed visit keeps its card for an hour so the customer can glance back
|
|
331
|
+
at it; a cancelled one has nothing worth showing and leaves at once.
|
|
391
332
|
|
|
392
|
-
|
|
333
|
+
Duration limits, stated separately: up to 8 hours active; after a
|
|
334
|
+
system-imposed end, up to 4 more hours on the Lock Screen (12 in total); after
|
|
335
|
+
an app-initiated end with `.default`, up to 4 hours of lingering.
|
|
393
336
|
|
|
394
|
-
|
|
395
|
-
represented accurately, publish a terminal state and end the activity instead of
|
|
396
|
-
leaving stale progress visible.
|
|
337
|
+
### Terminal server failure
|
|
397
338
|
|
|
398
339
|
```swift
|
|
399
|
-
func
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
vehicleDescription: message,
|
|
407
|
-
etaStartSeconds: Int(Date().timeIntervalSince1970),
|
|
408
|
-
etaEndSeconds: Int(Date().timeIntervalSince1970),
|
|
409
|
-
status: .failed,
|
|
410
|
-
distanceRemaining: 0
|
|
340
|
+
func failVisit(_ activity: Activity<ServiceVisitAttributes>, reason: String) async {
|
|
341
|
+
var last = activity.content.state
|
|
342
|
+
last.status = .failed
|
|
343
|
+
last.vanDescription = reason // a text field the view shows for failed visits
|
|
344
|
+
await activity.end(
|
|
345
|
+
ActivityContent(state: last, staleDate: nil, relevanceScore: 0),
|
|
346
|
+
dismissalPolicy: .immediate
|
|
411
347
|
)
|
|
412
|
-
|
|
413
|
-
let content = ActivityContent(state: failedState, staleDate: nil, relevanceScore: 0)
|
|
414
|
-
await activity.end(content, dismissalPolicy: .immediate)
|
|
415
348
|
}
|
|
416
349
|
```
|
|
417
350
|
|
|
418
|
-
###
|
|
351
|
+
### Sign-out
|
|
419
352
|
|
|
420
353
|
```swift
|
|
421
|
-
func
|
|
422
|
-
for
|
|
423
|
-
await
|
|
354
|
+
func endAllVisits() async {
|
|
355
|
+
for running in Activity<ServiceVisitAttributes>.activities {
|
|
356
|
+
await running.end(nil, dismissalPolicy: .immediate)
|
|
424
357
|
}
|
|
425
358
|
}
|
|
426
359
|
```
|
|
427
360
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
```swift
|
|
431
|
-
struct RideActivityWidget: Widget {
|
|
432
|
-
var body: some WidgetConfiguration {
|
|
433
|
-
ActivityConfiguration(for: RideAttributes.self) { context in
|
|
434
|
-
// Lock Screen presentation
|
|
435
|
-
RideLockScreenView(context: context)
|
|
436
|
-
} dynamicIsland: { context in
|
|
437
|
-
DynamicIsland {
|
|
438
|
-
// EXPANDED: shown on long-press
|
|
439
|
-
DynamicIslandExpandedRegion(.leading) {
|
|
440
|
-
VStack(alignment: .leading) {
|
|
441
|
-
Image(systemName: context.state.driverPhoto)
|
|
442
|
-
.font(.title2)
|
|
443
|
-
Text(context.state.driverName)
|
|
444
|
-
.font(.caption2)
|
|
445
|
-
.lineLimit(1)
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
DynamicIslandExpandedRegion(.trailing) {
|
|
450
|
-
VStack(alignment: .trailing) {
|
|
451
|
-
Text(timerInterval: context.state.etaRange, countsDown: true)
|
|
452
|
-
.font(.title3.monospacedDigit())
|
|
453
|
-
Text(String(format: "%.1f mi", context.state.distanceRemaining))
|
|
454
|
-
.font(.caption2)
|
|
455
|
-
.foregroundStyle(.secondary)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
361
|
+
Passing `nil` content ends the activity with its last state.
|
|
458
362
|
|
|
459
|
-
|
|
460
|
-
Text(context.state.status.displayName)
|
|
461
|
-
.font(.headline)
|
|
462
|
-
.lineLimit(1)
|
|
463
|
-
}
|
|
363
|
+
## Dynamic Island, all regions
|
|
464
364
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
.monospacedDigit()
|
|
494
|
-
} minimal: {
|
|
495
|
-
// MINIMAL: shown when multiple activities compete
|
|
496
|
-
Image(systemName: "car.fill")
|
|
497
|
-
.foregroundStyle(.green)
|
|
365
|
+
```swift
|
|
366
|
+
DynamicIsland {
|
|
367
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
368
|
+
Label {
|
|
369
|
+
Text(context.state.technicianName).lineLimit(1)
|
|
370
|
+
} icon: {
|
|
371
|
+
Image(systemName: context.state.avatarSymbol)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
375
|
+
VStack(alignment: .trailing) {
|
|
376
|
+
Text(timerInterval: context.state.arrivalWindow, countsDown: true)
|
|
377
|
+
.monospacedDigit()
|
|
378
|
+
Text(context.state.kilometresAway, format: .number.precision(.fractionLength(1)))
|
|
379
|
+
.font(.caption)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
DynamicIslandExpandedRegion(.center) {
|
|
383
|
+
Text(context.state.status.rawValue.capitalized)
|
|
384
|
+
}
|
|
385
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
386
|
+
VStack(spacing: 6) {
|
|
387
|
+
ProgressView(value: max(0, 15 - context.state.kilometresAway), total: 15)
|
|
388
|
+
.tint(.orange)
|
|
389
|
+
HStack {
|
|
390
|
+
Label(context.attributes.jobSummary, systemImage: "wrench.and.screwdriver")
|
|
391
|
+
Spacer()
|
|
392
|
+
Label(context.attributes.serviceAddress, systemImage: "house")
|
|
498
393
|
}
|
|
499
|
-
.
|
|
394
|
+
.font(.caption)
|
|
395
|
+
.lineLimit(1)
|
|
500
396
|
}
|
|
501
397
|
}
|
|
398
|
+
} compactLeading: {
|
|
399
|
+
Image(systemName: "wrench.and.screwdriver")
|
|
400
|
+
} compactTrailing: {
|
|
401
|
+
Text(timerInterval: context.state.arrivalWindow, countsDown: true)
|
|
402
|
+
.monospacedDigit()
|
|
403
|
+
.frame(width: 44)
|
|
404
|
+
} minimal: {
|
|
405
|
+
Image(systemName: "wrench.and.screwdriver").foregroundStyle(.orange)
|
|
502
406
|
}
|
|
407
|
+
.keylineTint(.orange)
|
|
503
408
|
```
|
|
504
409
|
|
|
505
|
-
## Lock Screen
|
|
410
|
+
## Lock Screen view with timer and progress
|
|
506
411
|
|
|
507
412
|
```swift
|
|
508
|
-
struct
|
|
509
|
-
let context: ActivityViewContext<
|
|
413
|
+
struct VisitLockScreenView: View {
|
|
414
|
+
let context: ActivityViewContext<ServiceVisitAttributes>
|
|
510
415
|
|
|
511
416
|
var body: some View {
|
|
512
|
-
VStack {
|
|
513
|
-
// Header
|
|
417
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
514
418
|
HStack {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
419
|
+
Text(context.state.technicianName).font(.headline)
|
|
420
|
+
Spacer()
|
|
421
|
+
if context.isStale {
|
|
422
|
+
Label("Waiting for fresh data", systemImage: "arrow.trianglehead.2.clockwise")
|
|
519
423
|
.font(.caption)
|
|
520
424
|
.foregroundStyle(.secondary)
|
|
425
|
+
} else {
|
|
426
|
+
Text(timerInterval: context.state.arrivalWindow, countsDown: true)
|
|
427
|
+
.monospacedDigit()
|
|
521
428
|
}
|
|
522
|
-
Spacer()
|
|
523
|
-
// Live countdown timer (auto-updating, no code needed)
|
|
524
|
-
Text(timerInterval: context.state.etaRange, countsDown: true)
|
|
525
|
-
.font(.title2.monospacedDigit().bold())
|
|
526
|
-
.foregroundStyle(.green)
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if context.isStale {
|
|
530
|
-
Label("Checking for updates...",
|
|
531
|
-
systemImage: "arrow.trianglehead.2.clockwise")
|
|
532
|
-
.font(.caption)
|
|
533
|
-
.foregroundStyle(.secondary)
|
|
534
429
|
}
|
|
535
|
-
|
|
536
|
-
// Progress bar
|
|
537
|
-
ProgressView(value: context.state.status.progress, total: 1.0)
|
|
538
|
-
.tint(.green)
|
|
539
|
-
|
|
540
|
-
// Route
|
|
430
|
+
ProgressView(value: max(0, 15 - context.state.kilometresAway), total: 15)
|
|
541
431
|
HStack {
|
|
542
|
-
|
|
543
|
-
Text("Pickup").font(.caption2).foregroundStyle(.secondary)
|
|
544
|
-
Text(context.attributes.pickupLocation).font(.caption).lineLimit(1)
|
|
545
|
-
}
|
|
432
|
+
Text(context.attributes.jobSummary)
|
|
546
433
|
Spacer()
|
|
547
|
-
|
|
548
|
-
.font(.caption)
|
|
549
|
-
.foregroundStyle(.secondary)
|
|
550
|
-
Spacer()
|
|
551
|
-
VStack(alignment: .trailing) {
|
|
552
|
-
Text("Dropoff").font(.caption2).foregroundStyle(.secondary)
|
|
553
|
-
Text(context.attributes.dropoffLocation).font(.caption).lineLimit(1)
|
|
554
|
-
}
|
|
434
|
+
Text(context.attributes.serviceAddress)
|
|
555
435
|
}
|
|
436
|
+
.font(.caption)
|
|
437
|
+
.lineLimit(1)
|
|
556
438
|
}
|
|
557
439
|
.padding()
|
|
558
440
|
}
|
|
559
441
|
}
|
|
560
442
|
```
|
|
561
443
|
|
|
562
|
-
|
|
444
|
+
`Text(timerInterval:countsDown:)` ticks by itself; no update is needed to keep
|
|
445
|
+
the countdown moving.
|
|
563
446
|
|
|
564
|
-
|
|
565
|
-
|
|
447
|
+
## Several activities at once
|
|
448
|
+
|
|
449
|
+
An app may run more than one Live Activity, up to a system limit. Keep
|
|
450
|
+
references yourself or ask `Activity<T>.activities`.
|
|
566
451
|
|
|
567
452
|
```swift
|
|
453
|
+
@preconcurrency import ActivityKit
|
|
454
|
+
import Observation
|
|
455
|
+
|
|
568
456
|
@Observable
|
|
569
457
|
@MainActor
|
|
570
|
-
final class
|
|
571
|
-
private(set) var
|
|
458
|
+
final class ChargingActivityCenter {
|
|
459
|
+
private(set) var bySession: [String: Activity<ChargingAttributes>] = [:]
|
|
572
460
|
|
|
573
|
-
func
|
|
574
|
-
state: DeliveryAttributes.ContentState) async throws {
|
|
575
|
-
let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 75)
|
|
461
|
+
func start(session: String, attributes: ChargingAttributes, state: ChargingAttributes.ContentState) throws {
|
|
576
462
|
let activity = try Activity.request(
|
|
577
|
-
attributes: attributes,
|
|
463
|
+
attributes: attributes,
|
|
464
|
+
content: ActivityContent(state: state, staleDate: nil),
|
|
465
|
+
pushType: .token
|
|
578
466
|
)
|
|
579
|
-
|
|
467
|
+
bySession[session] = activity
|
|
580
468
|
|
|
581
|
-
// Token forwarding
|
|
582
469
|
Task { [weak self] in
|
|
583
|
-
for await
|
|
584
|
-
|
|
585
|
-
try? await ServerAPI.shared.registerActivityToken(tokenString, orderID: orderID)
|
|
470
|
+
for await tokenData in activity.pushTokenUpdates {
|
|
471
|
+
try? await ChargingBackend.shared.register(tokenData.hexEncoded, session: session)
|
|
586
472
|
}
|
|
587
|
-
self?.
|
|
473
|
+
self?.bySession[session] = nil
|
|
588
474
|
}
|
|
589
475
|
}
|
|
590
476
|
|
|
591
|
-
func
|
|
592
|
-
|
|
593
|
-
let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 80)
|
|
594
|
-
await activity.update(content)
|
|
477
|
+
func update(session: String, to state: ChargingAttributes.ContentState) async {
|
|
478
|
+
await bySession[session]?.update(ActivityContent(state: state, staleDate: nil))
|
|
595
479
|
}
|
|
596
480
|
|
|
597
|
-
func
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
await activity.end(content, dismissalPolicy: .default)
|
|
601
|
-
activeDeliveries.removeValue(forKey: orderID)
|
|
481
|
+
func end(session: String) async {
|
|
482
|
+
await bySession[session]?.end(nil, dismissalPolicy: .default)
|
|
483
|
+
bySession[session] = nil
|
|
602
484
|
}
|
|
603
485
|
|
|
604
|
-
/// Reconcile in-memory state with system activities on app launch
|
|
605
486
|
func reconcile() {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if activeDeliveries[orderID] == nil {
|
|
610
|
-
activeDeliveries[orderID] = activity
|
|
611
|
-
}
|
|
487
|
+
for activity in Activity<ChargingAttributes>.activities
|
|
488
|
+
where !bySession.values.contains(where: { $0.id == activity.id }) {
|
|
489
|
+
bySession[activity.id] = activity // session unknown after relaunch; key by activity ID
|
|
612
490
|
}
|
|
613
491
|
}
|
|
614
492
|
}
|
|
615
493
|
```
|
|
616
494
|
|
|
617
|
-
|
|
495
|
+
Call `reconcile()` at launch so activities the system still holds are tracked
|
|
496
|
+
again after the process was killed.
|
|
497
|
+
|
|
498
|
+
`Activity` is not `Sendable`, and its `update` and `end` are `nonisolated`
|
|
499
|
+
async methods, so calling them on an activity held by a `@MainActor` type fails
|
|
500
|
+
in Swift 6 with "sending value of non-Sendable type 'Activity<...>' risks
|
|
501
|
+
causing data races". `@preconcurrency import ActivityKit` accepts that until
|
|
502
|
+
the SDK annotates the type; the alternative is to keep only activity IDs on
|
|
503
|
+
the main actor and do the calls from `nonisolated` code.
|
|
504
|
+
|
|
505
|
+
## Observing state and content
|
|
618
506
|
|
|
619
507
|
```swift
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
print("Activity is pending start")
|
|
630
|
-
case .stale:
|
|
631
|
-
// iOS 16.2+: staleDate passed without an update
|
|
632
|
-
print("Content is stale -- update or end")
|
|
633
|
-
case .ended:
|
|
634
|
-
// Ended but may still be visible on Lock Screen
|
|
635
|
-
print("Activity ended, may still linger on Lock Screen")
|
|
636
|
-
case .dismissed:
|
|
637
|
-
// Fully removed from UI -- safe to release resources
|
|
638
|
-
print("Activity dismissed from Lock Screen")
|
|
639
|
-
cleanupResources(for: activity.id)
|
|
640
|
-
@unknown default:
|
|
641
|
-
break
|
|
642
|
-
}
|
|
508
|
+
Task {
|
|
509
|
+
for await lifecycle in activity.activityStateUpdates {
|
|
510
|
+
switch lifecycle {
|
|
511
|
+
case .active: break // on screen and running
|
|
512
|
+
case .pending: break // iOS 26+: scheduled, not shown yet
|
|
513
|
+
case .stale: await refresh(activity) // staleDate passed with no update
|
|
514
|
+
case .ended: break // over, may still be on the Lock Screen
|
|
515
|
+
case .dismissed: cleanUp(activity.id) // gone; release resources here
|
|
516
|
+
@unknown default: break
|
|
643
517
|
}
|
|
644
518
|
}
|
|
519
|
+
}
|
|
645
520
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
print("New state: \(content.state)")
|
|
650
|
-
}
|
|
521
|
+
Task {
|
|
522
|
+
for await snapshot in activity.contentUpdates {
|
|
523
|
+
render(snapshot.state) // changes that arrived by push or from another process
|
|
651
524
|
}
|
|
652
525
|
}
|
|
653
526
|
```
|
|
654
527
|
|
|
655
|
-
|
|
528
|
+
`.stale` exists from iOS 16.2; `.pending` from iOS 26.
|
|
529
|
+
|
|
530
|
+
## Tokens
|
|
656
531
|
|
|
657
|
-
|
|
658
|
-
|
|
532
|
+
The update token is how the server reaches the activity, so a failed
|
|
533
|
+
registration deserves retries with exponential backoff rather than a silent
|
|
534
|
+
`try?`:
|
|
659
535
|
|
|
660
536
|
```swift
|
|
661
|
-
func
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
// Retry with exponential backoff; token is critical for updates
|
|
671
|
-
print("Failed to register token: \(error)")
|
|
672
|
-
}
|
|
537
|
+
func register(_ hex: String, activityID: String) async {
|
|
538
|
+
var backoff: Duration = .seconds(1)
|
|
539
|
+
for _ in 0..<5 {
|
|
540
|
+
do {
|
|
541
|
+
try await VisitBackend.shared.register(updateToken: hex, activityID: activityID)
|
|
542
|
+
return
|
|
543
|
+
} catch {
|
|
544
|
+
try? await Task.sleep(for: backoff)
|
|
545
|
+
backoff *= 2
|
|
673
546
|
}
|
|
674
547
|
}
|
|
675
548
|
}
|
|
549
|
+
```
|
|
676
550
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
551
|
+
Push-to-start tokens (iOS 17.2+) are separate from both the device token and
|
|
552
|
+
the per-activity update tokens:
|
|
553
|
+
|
|
554
|
+
```swift
|
|
555
|
+
Task {
|
|
556
|
+
for await tokenData in Activity<ServiceVisitAttributes>.pushToStartTokenUpdates {
|
|
557
|
+
try? await VisitBackend.shared.register(startToken: tokenData.hexEncoded)
|
|
685
558
|
}
|
|
686
559
|
}
|
|
687
560
|
```
|
|
688
561
|
|
|
689
|
-
## Authorization
|
|
562
|
+
## Authorization
|
|
690
563
|
|
|
691
|
-
|
|
692
|
-
|
|
564
|
+
The user can turn Live Activities off in Settings at any time, so check before
|
|
565
|
+
every start.
|
|
693
566
|
|
|
694
567
|
```swift
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
func observeAuthorization() {
|
|
706
|
-
Task {
|
|
707
|
-
let authInfo = ActivityAuthorizationInfo()
|
|
708
|
-
for await enabled in authInfo.activityEnablementUpdates {
|
|
709
|
-
if enabled {
|
|
710
|
-
observePushToStartToken()
|
|
711
|
-
} else {
|
|
712
|
-
try? await ServerAPI.shared.disableActivityPush()
|
|
713
|
-
}
|
|
568
|
+
let info = ActivityAuthorizationInfo()
|
|
569
|
+
let allowed = info.areActivitiesEnabled
|
|
570
|
+
let highCadence = info.frequentPushesEnabled
|
|
571
|
+
|
|
572
|
+
Task {
|
|
573
|
+
for await enabled in info.activityEnablementUpdates {
|
|
574
|
+
if enabled {
|
|
575
|
+
observePushToStartTokens()
|
|
576
|
+
} else {
|
|
577
|
+
await VisitBackend.shared.stopLiveActivityPushes()
|
|
714
578
|
}
|
|
715
579
|
}
|
|
580
|
+
}
|
|
716
581
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
try? await ServerAPI.shared.setFrequentPushesEnabled(frequentPushesEnabled)
|
|
721
|
-
}
|
|
582
|
+
Task {
|
|
583
|
+
for await enabled in info.frequentPushEnablementUpdates {
|
|
584
|
+
await VisitBackend.shared.setFrequentPushes(enabled)
|
|
722
585
|
}
|
|
723
586
|
}
|
|
724
587
|
```
|
|
725
588
|
|
|
726
|
-
##
|
|
589
|
+
## Errors
|
|
727
590
|
|
|
728
|
-
|
|
729
|
-
func startActivitySafely(
|
|
730
|
-
attributes: DeliveryAttributes,
|
|
731
|
-
state: DeliveryAttributes.ContentState
|
|
732
|
-
) async {
|
|
733
|
-
let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 75)
|
|
591
|
+
`Activity.request` throws `ActivityAuthorizationError`:
|
|
734
592
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
case .targetMaximumExceeded:
|
|
749
|
-
// Too many Live Activities for this app
|
|
750
|
-
print("App activity limit reached -- end an existing one first")
|
|
751
|
-
default:
|
|
752
|
-
print("Authorization error: \(error)")
|
|
753
|
-
}
|
|
754
|
-
} catch {
|
|
755
|
-
print("Unexpected error: \(error)")
|
|
593
|
+
```swift
|
|
594
|
+
do {
|
|
595
|
+
_ = try Activity.request(attributes: visit, content: opening, pushType: .token)
|
|
596
|
+
} catch let error as ActivityAuthorizationError {
|
|
597
|
+
switch error {
|
|
598
|
+
case .denied:
|
|
599
|
+
showLiveActivitiesOffHint() // user switched them off
|
|
600
|
+
case .globalMaximumExceeded:
|
|
601
|
+
logger.notice("Device-wide Live Activity limit reached")
|
|
602
|
+
case .targetMaximumExceeded:
|
|
603
|
+
await endOldestVisit() // this app has too many; free one first
|
|
604
|
+
default:
|
|
605
|
+
logger.error("Live Activity not started: \(error.localizedDescription)")
|
|
756
606
|
}
|
|
607
|
+
} catch {
|
|
608
|
+
logger.error("Unexpected request failure: \(error.localizedDescription)")
|
|
757
609
|
}
|
|
758
610
|
```
|
|
759
611
|
|
|
760
|
-
## Background
|
|
612
|
+
## Background behaviour
|
|
761
613
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
614
|
+
- Backgrounding or suspending the app does not remove the card.
|
|
615
|
+
- Live Activity views run in the widget extension's sandbox. They cannot hit
|
|
616
|
+
the network or receive location updates.
|
|
617
|
+
- Pushes are the main way to change an activity while the app is not running;
|
|
618
|
+
prefer them to Background App Refresh.
|
|
619
|
+
- A suspended app still receives these pushes, yet delivery, priority, budget
|
|
620
|
+
and throttling still apply. Combine `staleDate` with a reconcile pass when
|
|
621
|
+
the app returns to the foreground:
|
|
767
622
|
|
|
768
623
|
```swift
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
// Refresh from server in case pushes were missed
|
|
777
|
-
Task {
|
|
778
|
-
let serverState = try await ServerAPI.shared.fetchDeliveryState(
|
|
779
|
-
orderNumber: activity.attributes.orderNumber
|
|
780
|
-
)
|
|
781
|
-
let content = ActivityContent(
|
|
782
|
-
state: serverState,
|
|
783
|
-
staleDate: Date().addingTimeInterval(120),
|
|
784
|
-
relevanceScore: 80
|
|
785
|
-
)
|
|
786
|
-
await activity.update(content)
|
|
787
|
-
}
|
|
788
|
-
case .stale:
|
|
789
|
-
// Content is outdated -- update immediately
|
|
790
|
-
Task {
|
|
791
|
-
let serverState = try await ServerAPI.shared.fetchDeliveryState(
|
|
792
|
-
orderNumber: activity.attributes.orderNumber
|
|
793
|
-
)
|
|
794
|
-
let content = ActivityContent(
|
|
795
|
-
state: serverState,
|
|
796
|
-
staleDate: Date().addingTimeInterval(120),
|
|
797
|
-
relevanceScore: 80
|
|
798
|
-
)
|
|
799
|
-
await activity.update(content)
|
|
800
|
-
}
|
|
801
|
-
case .ended, .dismissed:
|
|
802
|
-
// Clean up local tracking
|
|
803
|
-
break
|
|
804
|
-
default:
|
|
805
|
-
break
|
|
624
|
+
func reconcileOnForeground() async {
|
|
625
|
+
for running in Activity<ServiceVisitAttributes>.activities {
|
|
626
|
+
switch running.activityState {
|
|
627
|
+
case .active: await refreshFromServer(running) // a push may have been missed
|
|
628
|
+
case .stale: await refreshFromServer(running) // update right away
|
|
629
|
+
case .ended, .dismissed: cleanUp(running.id)
|
|
630
|
+
default: break
|
|
806
631
|
}
|
|
807
632
|
}
|
|
808
633
|
}
|
|
809
634
|
```
|
|
810
635
|
|
|
811
|
-
|
|
812
|
-
Background App Refresh. Push updates can arrive while the app is suspended, but
|
|
813
|
-
APNs delivery, priority, budget, and throttling still apply; use `staleDate` and
|
|
814
|
-
foreground reconciliation for missed updates.
|
|
815
|
-
|
|
816
|
-
## Testing in Simulator and on Device
|
|
817
|
-
|
|
818
|
-
### Simulator
|
|
636
|
+
## Testing
|
|
819
637
|
|
|
820
|
-
The
|
|
821
|
-
the Dynamic Island
|
|
822
|
-
previews for rapid iteration:
|
|
638
|
+
The simulator shows Live Activities on its Lock Screen and, on models that
|
|
639
|
+
have one, in the Dynamic Island. Xcode previews cover each presentation:
|
|
823
640
|
|
|
824
641
|
```swift
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
driverName: "Alex",
|
|
830
|
-
driverPhoto: "car.fill",
|
|
831
|
-
vehicleDescription: "White Toyota Camry",
|
|
832
|
-
etaStartSeconds: Int(Date().timeIntervalSince1970),
|
|
833
|
-
etaEndSeconds: Int(Date().addingTimeInterval(300).timeIntervalSince1970),
|
|
834
|
-
status: .driverEnRoute,
|
|
835
|
-
distanceRemaining: 1.5
|
|
836
|
-
)
|
|
837
|
-
RideAttributes.ContentState(
|
|
838
|
-
driverName: "Alex",
|
|
839
|
-
driverPhoto: "car.fill",
|
|
840
|
-
vehicleDescription: "White Toyota Camry",
|
|
841
|
-
etaStartSeconds: Int(Date().timeIntervalSince1970),
|
|
842
|
-
etaEndSeconds: Int(Date().addingTimeInterval(60).timeIntervalSince1970),
|
|
843
|
-
status: .arriving,
|
|
844
|
-
distanceRemaining: 0.1
|
|
845
|
-
)
|
|
642
|
+
extension ServiceVisitAttributes {
|
|
643
|
+
static var preview: ServiceVisitAttributes {
|
|
644
|
+
ServiceVisitAttributes(customerFirstName: "Alex", serviceAddress: "12 Elm Row", jobSummary: "Boiler check")
|
|
645
|
+
}
|
|
846
646
|
}
|
|
847
647
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
etaStartSeconds: Int(Date().timeIntervalSince1970),
|
|
856
|
-
etaEndSeconds: Int(Date().addingTimeInterval(300).timeIntervalSince1970),
|
|
857
|
-
status: .driverEnRoute,
|
|
858
|
-
distanceRemaining: 1.5
|
|
859
|
-
)
|
|
648
|
+
extension ServiceVisitAttributes.ContentState {
|
|
649
|
+
static let enRoute = Self(technicianName: "Sam", avatarSymbol: "person.crop.circle",
|
|
650
|
+
vanDescription: "White van", arrivalStartEpoch: 1790000300, arrivalEndEpoch: 1790000900,
|
|
651
|
+
status: .enRoute, kilometresAway: 9)
|
|
652
|
+
static let onSite = Self(technicianName: "Sam", avatarSymbol: "person.crop.circle",
|
|
653
|
+
vanDescription: "White van", arrivalStartEpoch: 1790000300, arrivalEndEpoch: 1790000300,
|
|
654
|
+
status: .onSite, kilometresAway: 0)
|
|
860
655
|
}
|
|
861
656
|
|
|
862
|
-
#Preview("
|
|
863
|
-
|
|
657
|
+
#Preview("Visit card", as: .content, using: ServiceVisitAttributes.preview) {
|
|
658
|
+
ServiceVisitLiveActivity()
|
|
864
659
|
} contentStates: {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
driverPhoto: "car.fill",
|
|
868
|
-
vehicleDescription: "White Toyota Camry",
|
|
869
|
-
etaStartSeconds: Int(Date().timeIntervalSince1970),
|
|
870
|
-
etaEndSeconds: Int(Date().addingTimeInterval(300).timeIntervalSince1970),
|
|
871
|
-
status: .driverEnRoute,
|
|
872
|
-
distanceRemaining: 1.5
|
|
873
|
-
)
|
|
660
|
+
ServiceVisitAttributes.ContentState.enRoute
|
|
661
|
+
ServiceVisitAttributes.ContentState.onSite
|
|
874
662
|
}
|
|
875
663
|
|
|
876
|
-
#Preview("
|
|
877
|
-
|
|
664
|
+
#Preview("Expanded", as: .dynamicIsland(.expanded), using: ServiceVisitAttributes.preview) {
|
|
665
|
+
ServiceVisitLiveActivity()
|
|
878
666
|
} contentStates: {
|
|
879
|
-
|
|
880
|
-
driverName: "Alex",
|
|
881
|
-
driverPhoto: "car.fill",
|
|
882
|
-
vehicleDescription: "White Toyota Camry",
|
|
883
|
-
etaStartSeconds: Int(Date().timeIntervalSince1970),
|
|
884
|
-
etaEndSeconds: Int(Date().addingTimeInterval(300).timeIntervalSince1970),
|
|
885
|
-
status: .driverEnRoute,
|
|
886
|
-
distanceRemaining: 1.5
|
|
887
|
-
)
|
|
888
|
-
}
|
|
889
|
-
```
|
|
890
|
-
|
|
891
|
-
### Preview Data Helper
|
|
892
|
-
|
|
893
|
-
```swift
|
|
894
|
-
extension RideAttributes {
|
|
895
|
-
static var preview: RideAttributes {
|
|
896
|
-
RideAttributes(
|
|
897
|
-
riderName: "Jordan",
|
|
898
|
-
pickupLocation: "123 Main St",
|
|
899
|
-
dropoffLocation: "456 Oak Ave"
|
|
900
|
-
)
|
|
901
|
-
}
|
|
667
|
+
ServiceVisitAttributes.ContentState.enRoute
|
|
902
668
|
}
|
|
903
669
|
```
|
|
904
670
|
|
|
905
|
-
|
|
671
|
+
Use `.dynamicIsland(.compact)` and `.dynamicIsland(.minimal)` the same way.
|
|
672
|
+
Listing several states in `contentStates:` lets the preview step through the
|
|
673
|
+
transitions between them.
|
|
906
674
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
675
|
+
The simulator has no APNs, so push-to-update is tested on a physical device.
|
|
676
|
+
Send a test push with a push testing tool or with curl. Development builds use
|
|
677
|
+
the sandbox host `api.sandbox.push.apple.com`; production builds use
|
|
678
|
+
`api.push.apple.com`:
|
|
910
679
|
|
|
911
680
|
```bash
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
--data '{"aps":{"timestamp":1700000000,"event":"update","content-state":{"driverName":"Alex","driverPhoto":"car.fill","vehicleDescription":"White Toyota Camry","etaStartSeconds":1700000000,"etaEndSeconds":1700000300,"status":"driverArrived","distanceRemaining":0.0},"alert":{"title":"Driver Arrived","body":"Your driver is here!"}}}' \
|
|
920
|
-
https://api.push.apple.com/3/device/$DEVICE_PUSH_TOKEN
|
|
681
|
+
curl --http2 \
|
|
682
|
+
-H "apns-push-type: liveactivity" \
|
|
683
|
+
-H "apns-topic: com.example.fieldservice.push-type.liveactivity" \
|
|
684
|
+
-H "apns-priority: 10" \
|
|
685
|
+
-H "authorization: bearer $JWT_TOKEN" \
|
|
686
|
+
-d @update.json \
|
|
687
|
+
"https://api.sandbox.push.apple.com/3/device/$ACTIVITY_PUSH_TOKEN"
|
|
921
688
|
```
|
|
922
689
|
|
|
923
|
-
### Debugging
|
|
690
|
+
### Debugging
|
|
924
691
|
|
|
925
|
-
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
-
|
|
930
|
-
-
|
|
931
|
-
- If
|
|
932
|
-
|
|
692
|
+
- A start that fails usually explains itself in Console.app under the
|
|
693
|
+
`ActivityKit` subsystem.
|
|
694
|
+
- Compare the `content-state` keys with the synthesized `Codable` shape, or
|
|
695
|
+
with the agreed `CodingKeys`.
|
|
696
|
+
- In the debugger, `Activity<T>.activities` lists what is running.
|
|
697
|
+
- A breakpoint inside the `pushTokenUpdates` loop shows whether tokens arrive.
|
|
698
|
+
- If nothing ever appears, confirm that the main app target (not just the
|
|
699
|
+
extension) declares `NSSupportsLiveActivities = YES`.
|
|
933
700
|
|
|
934
|
-
## Info.plist
|
|
701
|
+
## Info.plist keys
|
|
935
702
|
|
|
936
|
-
| Key | Value |
|
|
703
|
+
| Key | Value | Effect |
|
|
937
704
|
|---|---|---|
|
|
938
|
-
| `NSSupportsLiveActivities` | `YES` |
|
|
939
|
-
| `NSSupportsLiveActivitiesFrequentUpdates` | `YES` |
|
|
940
|
-
|
|
941
|
-
Both keys
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
- [
|
|
947
|
-
- [
|
|
948
|
-
- [
|
|
949
|
-
- [
|
|
950
|
-
- [
|
|
951
|
-
- [
|
|
952
|
-
- [
|
|
953
|
-
- [
|
|
705
|
+
| `NSSupportsLiveActivities` | `YES` | Required for the app to start Live Activities |
|
|
706
|
+
| `NSSupportsLiveActivitiesFrequentUpdates` | `YES` | Raises the system-managed push budget |
|
|
707
|
+
|
|
708
|
+
Both keys are read from the main app target; adding them to the extension has
|
|
709
|
+
no effect.
|
|
710
|
+
|
|
711
|
+
## Apple documentation
|
|
712
|
+
|
|
713
|
+
- [ActivityKit](https://developer.apple.com/documentation/activitykit)
|
|
714
|
+
- [ActivityAttributes](https://developer.apple.com/documentation/activitykit/activityattributes)
|
|
715
|
+
- [Activity](https://developer.apple.com/documentation/activitykit/activity)
|
|
716
|
+
- [ActivityContent](https://developer.apple.com/documentation/activitykit/activitycontent)
|
|
717
|
+
- [ActivityAuthorizationInfo](https://developer.apple.com/documentation/activitykit/activityauthorizationinfo)
|
|
718
|
+
- [DynamicIsland](https://developer.apple.com/documentation/widgetkit/dynamicisland)
|
|
719
|
+
- [ActivityConfiguration](https://developer.apple.com/documentation/widgetkit/activityconfiguration)
|
|
720
|
+
- [Live Activity push notifications guide](https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications)
|
|
721
|
+
- [Broadcast push requests to APNs](https://developer.apple.com/documentation/usernotifications/sending-broadcast-push-notification-requests-to-apns)
|