@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,276 +1,281 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: push-notifications
|
|
3
|
-
description: "
|
|
3
|
+
description: "UserNotifications and APNs on iOS and macOS, local and remote: permission (standard, provisional, critical), device-token registration, triggers, payloads (alert, background, mutable, localized), foreground presentation, tap handling and deep links, categories and actions, grouping, service and content extensions, media attachments, communication notifications, delivery debugging. Use when building, reviewing or debugging notifications, badges, sounds, silent pushes or notification permission. Not for Live Activity, VoIP or App Clip pushes, or post-push background work."
|
|
4
4
|
metadata:
|
|
5
|
-
source:
|
|
5
|
+
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Push Notifications
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
10
|
+
Notifications on Apple platforms come from two places: the device itself
|
|
11
|
+
(local notifications scheduled through `UserNotifications`) and a server that
|
|
12
|
+
talks to the Apple Push Notification service (APNs). Both end up in the same
|
|
13
|
+
`UNUserNotificationCenter`, so permission, presentation, actions and grouping
|
|
14
|
+
work the same way for each.
|
|
15
|
+
|
|
16
|
+
Baseline: iOS 26 and Swift 6.3. Everything here also works back to iOS 16
|
|
17
|
+
unless a section names a newer release.
|
|
18
|
+
|
|
19
|
+
Hand these to their own skills:
|
|
20
|
+
|
|
21
|
+
| Topic | Skill |
|
|
22
|
+
|-------|-------|
|
|
23
|
+
| Live Activity `content-state` pushes | `live-activities` |
|
|
24
|
+
| PushKit and VoIP call pushes | `callkit-voip` |
|
|
25
|
+
| Ephemeral App Clip notifications | `app-clips` |
|
|
26
|
+
| Scheduled or long background work triggered by a push | `background-processing` |
|
|
27
|
+
|
|
28
|
+
Longer material:
|
|
29
|
+
the [app-side patterns](references/notification-patterns.md) (launch wiring,
|
|
30
|
+
the delegate, routing taps to screens, silent-push handling, a scheduler, the
|
|
31
|
+
token lifecycle, testing, badges) and the
|
|
32
|
+
[extensions guide](references/rich-notifications.md) (service and content
|
|
33
|
+
extensions, attachments, communication notifications).
|
|
34
|
+
|
|
35
|
+
## Reviewing Someone Else's Design
|
|
36
|
+
|
|
37
|
+
When a proposal is wrong, say which contract it breaks, then give the fix.
|
|
38
|
+
"Move this call" is not enough; "registration does not depend on alert
|
|
39
|
+
permission, so this gate blocks silent pushes" is. The contracts that come up
|
|
40
|
+
most often:
|
|
41
|
+
|
|
42
|
+
**Device token**
|
|
43
|
+
- Getting an APNs token and getting alert permission are two separate things.
|
|
44
|
+
- Send the token to the server from every `didRegister` callback.
|
|
45
|
+
- A token saved on the device is a cache, never the truth.
|
|
46
|
+
- Token length is not fixed; never hard-code or validate a size.
|
|
47
|
+
- On Simulator, registration fails by design. Pushes can still be simulated
|
|
48
|
+
with a `.apns` file or `xcrun simctl push`.
|
|
49
|
+
|
|
50
|
+
**Background (silent) push**
|
|
51
|
+
- Payload carries `content-available: 1` and nothing that alerts: no alert, sound or badge.
|
|
52
|
+
- Send it at priority 5 (`apns-priority: 5`) with the header `apns-push-type: background`.
|
|
53
|
+
- Turn on Remote notifications under the app's Background Modes capability.
|
|
54
|
+
- APNs treats it as low priority and throttles it; it may never arrive. It is
|
|
55
|
+
not a scheduler, so a cadence of one every few minutes will not happen.
|
|
56
|
+
- `didReceiveRemoteNotification` does short, bounded work and returns the
|
|
57
|
+
`UIBackgroundFetchResult` that matches what actually happened.
|
|
58
|
+
|
|
59
|
+
**Rich notifications**
|
|
60
|
+
- A service extension runs only for an alerting push with `mutable-content: 1`.
|
|
61
|
+
A silent push never reaches it.
|
|
62
|
+
- Attachments must be supported files already on disk; the system checks them
|
|
63
|
+
and moves them into its own store.
|
|
64
|
+
- Secrets such as decryption keys live in a shared Keychain access group. App
|
|
65
|
+
Groups are for shared files and `UserDefaults` only.
|
|
66
|
+
- Communication notifications need four things: the capability, an
|
|
67
|
+
`NSUserActivityTypes` entry, a donated `INInteraction`, and a call to
|
|
68
|
+
`content.updating(from:)`.
|
|
69
|
+
- The content handler is called exactly once on every path: success, failed
|
|
70
|
+
download, failed decryption, and `serviceExtensionTimeWillExpire()`. It
|
|
71
|
+
receives the updated content, the best attempt so far, or the original.
|
|
72
|
+
|
|
73
|
+
## Permission
|
|
74
|
+
|
|
75
|
+
Ask before scheduling anything the user will see or hear, or before setting a
|
|
76
|
+
badge. iOS shows the system prompt a single time; after that the call returns
|
|
77
|
+
whatever the user chose. Registering with APNs is a different path and may
|
|
78
|
+
happen even if the user said no to alerts.
|
|
35
79
|
|
|
36
80
|
```swift
|
|
37
|
-
import UserNotifications
|
|
38
|
-
|
|
39
81
|
@MainActor
|
|
40
|
-
func
|
|
41
|
-
let center = UNUserNotificationCenter.current()
|
|
82
|
+
func askForAlertPermission() async -> Bool {
|
|
42
83
|
do {
|
|
43
|
-
|
|
44
|
-
options: [.alert, .sound, .badge]
|
|
45
|
-
)
|
|
46
|
-
return granted
|
|
84
|
+
return try await UNUserNotificationCenter.current()
|
|
85
|
+
.requestAuthorization(options: [.alert, .sound, .badge])
|
|
47
86
|
} catch {
|
|
48
|
-
print("Authorization request failed: \(error)")
|
|
49
87
|
return false
|
|
50
88
|
}
|
|
51
89
|
}
|
|
52
90
|
```
|
|
53
91
|
|
|
54
|
-
###
|
|
92
|
+
### Reading the current setting
|
|
55
93
|
|
|
56
|
-
|
|
94
|
+
The user can change the setting in Settings at any moment, so read it instead
|
|
95
|
+
of remembering an old answer.
|
|
57
96
|
|
|
58
97
|
```swift
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
let center = UNUserNotificationCenter.current()
|
|
99
|
+
let current = await center.notificationSettings()
|
|
100
|
+
switch current.authorizationStatus {
|
|
101
|
+
case .notDetermined: break // never asked
|
|
102
|
+
case .denied: break // user said no
|
|
103
|
+
case .authorized: break
|
|
104
|
+
case .provisional: break // quiet delivery, no prompt shown
|
|
105
|
+
case .ephemeral: break // App Clip
|
|
106
|
+
@unknown default: break
|
|
64
107
|
}
|
|
65
108
|
```
|
|
66
109
|
|
|
67
|
-
### Provisional
|
|
68
|
-
|
|
69
|
-
Provisional notifications deliver quietly to the notification center without interrupting the user. The user can then choose to keep or turn them off. Use for onboarding flows where you want to demonstrate value before asking for full permission.
|
|
110
|
+
### Provisional authorization
|
|
70
111
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
Adding `.provisional` to the options skips the prompt. Notifications go
|
|
113
|
+
straight to Notification Center without sound or banner, and the user decides
|
|
114
|
+
later whether to keep them. It suits onboarding: show value first, ask for full
|
|
115
|
+
permission afterwards.
|
|
75
116
|
|
|
76
|
-
### Critical
|
|
117
|
+
### Critical alerts
|
|
77
118
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
try await center.requestAuthorization(
|
|
83
|
-
options: [.alert, .sound, .badge, .criticalAlert]
|
|
84
|
-
)
|
|
85
|
-
```
|
|
119
|
+
`.criticalAlert` plays through Do Not Disturb and the ring/silent switch. It
|
|
120
|
+
needs the `com.apple.developer.usernotifications.critical-alerts` entitlement,
|
|
121
|
+
which Apple grants on request through the developer portal. Reserve it for
|
|
122
|
+
health, safety and security situations.
|
|
86
123
|
|
|
87
|
-
###
|
|
124
|
+
### When the user said no
|
|
88
125
|
|
|
89
|
-
|
|
126
|
+
Do not ask again and do not nag. Offer a button that opens the app's page in
|
|
127
|
+
Settings through `UIApplication.openSettingsURLString`.
|
|
90
128
|
|
|
91
|
-
## APNs
|
|
129
|
+
## Registering with APNs
|
|
92
130
|
|
|
93
|
-
|
|
131
|
+
A SwiftUI app still needs an app delegate for this: the device token is only
|
|
132
|
+
delivered to `UIApplicationDelegate` callbacks. Attach one with
|
|
133
|
+
`@UIApplicationDelegateAdaptor`.
|
|
94
134
|
|
|
95
135
|
```swift
|
|
96
|
-
|
|
97
|
-
struct MyApp: App {
|
|
98
|
-
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
99
|
-
|
|
100
|
-
var body: some Scene {
|
|
101
|
-
WindowGroup {
|
|
102
|
-
ContentView()
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
class AppDelegate: NSObject, UIApplicationDelegate {
|
|
136
|
+
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
108
137
|
func application(
|
|
109
138
|
_ application: UIApplication,
|
|
110
|
-
didFinishLaunchingWithOptions
|
|
139
|
+
didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
111
140
|
) -> Bool {
|
|
112
|
-
UNUserNotificationCenter.current().delegate =
|
|
141
|
+
UNUserNotificationCenter.current().delegate = InboxNotificationDelegate.shared
|
|
113
142
|
return true
|
|
114
143
|
}
|
|
115
144
|
|
|
116
|
-
func application(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
121
|
-
print("APNs token: \(token)")
|
|
122
|
-
// Send token to your server
|
|
123
|
-
Task { await TokenService.shared.upload(token: token) }
|
|
145
|
+
func application(_ application: UIApplication,
|
|
146
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
147
|
+
let hex = deviceToken.reduce(into: "") { $0 += String(format: "%02x", $1) }
|
|
148
|
+
Task { await DeviceTokenUploader.shared.send(hex) }
|
|
124
149
|
}
|
|
125
150
|
|
|
126
|
-
func application(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
) {
|
|
130
|
-
print("APNs registration failed: \(error.localizedDescription)")
|
|
131
|
-
// Simulator can simulate pushes, but it does not register with APNs.
|
|
151
|
+
func application(_ application: UIApplication,
|
|
152
|
+
didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
153
|
+
// Expected on Simulator: it can show simulated pushes but never gets an APNs token.
|
|
132
154
|
}
|
|
133
155
|
}
|
|
134
156
|
```
|
|
135
157
|
|
|
136
|
-
###
|
|
158
|
+
### Order of calls
|
|
137
159
|
|
|
138
|
-
|
|
160
|
+
1. At launch: set the delegate and register categories.
|
|
161
|
+
2. At a moment that makes sense to the user: ask for alert permission.
|
|
162
|
+
3. Whenever a token is needed: call `registerForRemoteNotifications()`.
|
|
163
|
+
|
|
164
|
+
Do not wait for `.authorized` before step 3. An app without alert permission
|
|
165
|
+
still receives remote notifications, just silently, and the server still needs
|
|
166
|
+
the token.
|
|
139
167
|
|
|
140
168
|
```swift
|
|
141
169
|
@MainActor
|
|
142
|
-
func
|
|
170
|
+
func prepareNotifications() async {
|
|
143
171
|
let center = UNUserNotificationCenter.current()
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if settings.authorizationStatus == .notDetermined {
|
|
147
|
-
_ = await requestNotificationPermission()
|
|
172
|
+
if await center.notificationSettings().authorizationStatus == .notDetermined {
|
|
173
|
+
_ = try? await center.requestAuthorization(options: [.alert, .badge, .sound])
|
|
148
174
|
}
|
|
149
|
-
|
|
150
|
-
// Needed for APNs token delivery and silent remote notifications.
|
|
151
|
-
UIApplication.shared.registerForRemoteNotifications()
|
|
175
|
+
UIApplication.shared.registerForRemoteNotifications() // token + silent pushes
|
|
152
176
|
}
|
|
153
177
|
```
|
|
154
178
|
|
|
155
|
-
### Token
|
|
179
|
+
### Token rules
|
|
156
180
|
|
|
157
|
-
|
|
181
|
+
The token can change (restore, reinstall, new device, OS update). Upload it
|
|
182
|
+
from every `didRegisterForRemoteNotificationsWithDeviceToken` call, not only the
|
|
183
|
+
first one. Do not keep a local copy as the source of truth, and do not assume a
|
|
184
|
+
length.
|
|
158
185
|
|
|
159
186
|
## Local Notifications
|
|
160
187
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
### Creating Content
|
|
188
|
+
Local notifications need no server: the device schedules and shows them.
|
|
189
|
+
Reminders, timers and arriving at a place are the usual cases.
|
|
164
190
|
|
|
165
191
|
```swift
|
|
166
192
|
let content = UNMutableNotificationContent()
|
|
167
|
-
content.title = "
|
|
168
|
-
content.subtitle = "
|
|
169
|
-
content.body = "
|
|
193
|
+
content.title = "Plants"
|
|
194
|
+
content.subtitle = "Balcony"
|
|
195
|
+
content.body = "The basil has not been watered for three days."
|
|
170
196
|
content.sound = .default
|
|
171
197
|
content.badge = NSNumber(value: 1)
|
|
172
|
-
content.userInfo = ["
|
|
173
|
-
content.threadIdentifier = "
|
|
198
|
+
content.userInfo = ["plantID": "basil-01"]
|
|
199
|
+
content.threadIdentifier = "garden" // groups these together in Notification Center
|
|
174
200
|
```
|
|
175
201
|
|
|
176
|
-
###
|
|
202
|
+
### Triggers
|
|
177
203
|
|
|
178
204
|
```swift
|
|
179
|
-
//
|
|
180
|
-
let
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
var
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
let
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
center: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.01),
|
|
193
|
-
radius: 100,
|
|
205
|
+
// Once, 15 minutes from now. A repeating interval must be at least 60 seconds.
|
|
206
|
+
let soon = UNTimeIntervalNotificationTrigger(timeInterval: 900, repeats: false)
|
|
207
|
+
|
|
208
|
+
// Every day at 07:30.
|
|
209
|
+
var morning = DateComponents()
|
|
210
|
+
morning.hour = 7
|
|
211
|
+
morning.minute = 30
|
|
212
|
+
let daily = UNCalendarNotificationTrigger(dateMatching: morning, repeats: true)
|
|
213
|
+
|
|
214
|
+
// On arriving at a place. Needs at least When In Use location permission.
|
|
215
|
+
let gym = CLCircularRegion(
|
|
216
|
+
center: CLLocationCoordinate2D(latitude: 52.37, longitude: 4.89),
|
|
217
|
+
radius: 150,
|
|
194
218
|
identifier: "gym"
|
|
195
219
|
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
let
|
|
199
|
-
// Requires "When In Use" location permission at minimum
|
|
220
|
+
gym.notifyOnEntry = true
|
|
221
|
+
gym.notifyOnExit = false
|
|
222
|
+
let arrival = UNLocationNotificationTrigger(region: gym, repeats: false)
|
|
200
223
|
```
|
|
201
224
|
|
|
202
|
-
### Scheduling and
|
|
225
|
+
### Scheduling and cleanup
|
|
203
226
|
|
|
204
227
|
```swift
|
|
205
|
-
let request = UNNotificationRequest(
|
|
206
|
-
identifier: "workout-reminder-abc123",
|
|
207
|
-
content: content,
|
|
208
|
-
trigger: timeTrigger
|
|
209
|
-
)
|
|
210
|
-
|
|
211
228
|
let center = UNUserNotificationCenter.current()
|
|
229
|
+
let request = UNNotificationRequest(identifier: "water-basil", content: content, trigger: daily)
|
|
212
230
|
try await center.add(request)
|
|
213
231
|
|
|
214
|
-
|
|
215
|
-
center.removePendingNotificationRequests(withIdentifiers: ["workout-reminder-abc123"])
|
|
232
|
+
let waiting = await center.pendingNotificationRequests()
|
|
216
233
|
|
|
217
|
-
|
|
234
|
+
center.removePendingNotificationRequests(withIdentifiers: ["water-basil"])
|
|
218
235
|
center.removeAllPendingNotificationRequests()
|
|
219
|
-
|
|
220
|
-
// Remove delivered notifications from notification center
|
|
221
|
-
center.removeDeliveredNotifications(withIdentifiers: ["workout-reminder-abc123"])
|
|
236
|
+
center.removeDeliveredNotifications(withIdentifiers: ["water-basil"])
|
|
222
237
|
center.removeAllDeliveredNotifications()
|
|
223
|
-
|
|
224
|
-
// List all pending requests
|
|
225
|
-
let pending = await center.pendingNotificationRequests()
|
|
226
238
|
```
|
|
227
239
|
|
|
228
|
-
## Remote
|
|
240
|
+
## Remote Payloads
|
|
229
241
|
|
|
230
|
-
###
|
|
242
|
+
### Alert push
|
|
231
243
|
|
|
232
244
|
```json
|
|
233
245
|
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
"thread-id": "chat-alice",
|
|
243
|
-
"category": "MESSAGE_CATEGORY"
|
|
244
|
-
},
|
|
245
|
-
"messageId": "msg-789",
|
|
246
|
-
"senderId": "user-alice"
|
|
246
|
+
"aps": {
|
|
247
|
+
"alert": { "title": "Order shipped", "subtitle": "#4821", "body": "Arrives Thursday." },
|
|
248
|
+
"badge": 2,
|
|
249
|
+
"sound": "default",
|
|
250
|
+
"thread-id": "orders",
|
|
251
|
+
"category": "ORDER_UPDATE"
|
|
252
|
+
},
|
|
253
|
+
"orderID": "4821"
|
|
247
254
|
}
|
|
248
255
|
```
|
|
249
256
|
|
|
250
|
-
|
|
257
|
+
App-specific keys go beside `aps`, never inside it.
|
|
251
258
|
|
|
252
|
-
|
|
259
|
+
### Background push
|
|
253
260
|
|
|
254
261
|
```json
|
|
255
|
-
{
|
|
256
|
-
"aps": {
|
|
257
|
-
"content-available": 1
|
|
258
|
-
},
|
|
259
|
-
"updateType": "new-data"
|
|
260
|
-
}
|
|
262
|
+
{ "aps": { "content-available": 1 }, "syncScope": "inbox" }
|
|
261
263
|
```
|
|
262
264
|
|
|
263
|
-
|
|
265
|
+
- No `alert`, `sound` or `badge`.
|
|
266
|
+
- Background Modes > Remote notifications must be on.
|
|
267
|
+
- Header `apns-push-type: background`, priority `apns-priority: 5`.
|
|
268
|
+
- Expect throttling and occasional drops. A push every few minutes will be
|
|
269
|
+
held back, and data freshness must not hinge on one arriving.
|
|
270
|
+
- Finish quickly and return a result inside the short background window.
|
|
271
|
+
|
|
264
272
|
```swift
|
|
265
|
-
func application(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
guard let updateType = userInfo["updateType"] as? String else {
|
|
270
|
-
return .noData
|
|
271
|
-
}
|
|
273
|
+
func application(_ application: UIApplication,
|
|
274
|
+
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async
|
|
275
|
+
-> UIBackgroundFetchResult {
|
|
276
|
+
guard let scope = userInfo["syncScope"] as? String else { return .noData }
|
|
272
277
|
do {
|
|
273
|
-
try await
|
|
278
|
+
try await MailboxSync.shared.refresh(scope: scope)
|
|
274
279
|
return .newData
|
|
275
280
|
} catch {
|
|
276
281
|
return .failed
|
|
@@ -278,223 +283,190 @@ func application(
|
|
|
278
283
|
}
|
|
279
284
|
```
|
|
280
285
|
|
|
281
|
-
### Mutable
|
|
286
|
+
### Mutable content
|
|
282
287
|
|
|
283
|
-
|
|
288
|
+
Adding `mutable-content: 1` to an alerting payload hands it to your
|
|
289
|
+
Notification Service Extension before it is shown. Silent pushes do not start
|
|
290
|
+
the extension.
|
|
284
291
|
|
|
285
292
|
```json
|
|
286
293
|
{
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
"aps": {
|
|
295
|
+
"alert": { "title": "New photo", "body": "Ada shared a picture." },
|
|
296
|
+
"mutable-content": 1
|
|
297
|
+
},
|
|
298
|
+
"mediaURL": "https://media.example.com/p/77.jpg"
|
|
292
299
|
}
|
|
293
300
|
```
|
|
294
301
|
|
|
295
|
-
|
|
302
|
+
Good jobs for the extension, all kept short: fetch a supported attachment to
|
|
303
|
+
disk, decrypt the visible text, and turn the push into a communication
|
|
304
|
+
notification (capability, `NSUserActivityTypes`, `INInteraction` donation, then
|
|
305
|
+
`content.updating(from:)`). Every outcome, including timeout, ends with exactly
|
|
306
|
+
one call to the content handler. See the
|
|
307
|
+
[extensions guide](references/rich-notifications.md#service-extension).
|
|
296
308
|
|
|
297
|
-
|
|
309
|
+
### Localized payloads
|
|
310
|
+
|
|
311
|
+
Let the device pick the language by sending keys instead of text:
|
|
298
312
|
|
|
299
313
|
```json
|
|
300
|
-
{
|
|
301
|
-
"aps": {
|
|
302
|
-
"alert": {
|
|
303
|
-
"title-loc-key": "NEW_MESSAGE_TITLE",
|
|
304
|
-
"loc-key": "NEW_MESSAGE_BODY",
|
|
305
|
-
"loc-args": ["Alice"]
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
314
|
+
{ "aps": { "alert": { "title-loc-key": "SHIPPED_TITLE", "loc-key": "SHIPPED_BODY", "loc-args": ["Thursday"] } } }
|
|
309
315
|
```
|
|
310
316
|
|
|
311
|
-
##
|
|
317
|
+
## Handling Notifications
|
|
312
318
|
|
|
313
|
-
###
|
|
319
|
+
### The delegate
|
|
314
320
|
|
|
315
|
-
|
|
321
|
+
Install `UNUserNotificationCenter.current().delegate` before launch finishes,
|
|
322
|
+
from `App.init` or from `application(_:didFinishLaunchingWithOptions:)`.
|
|
316
323
|
|
|
317
324
|
```swift
|
|
318
325
|
@MainActor
|
|
319
|
-
final class
|
|
320
|
-
static let shared =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
) async -> UNNotificationPresentationOptions {
|
|
327
|
-
// Return which presentation elements to show
|
|
328
|
-
// Without this, foreground notifications are silently suppressed
|
|
329
|
-
return [.banner, .sound, .badge]
|
|
326
|
+
final class InboxNotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|
327
|
+
static let shared = InboxNotificationDelegate()
|
|
328
|
+
|
|
329
|
+
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
330
|
+
willPresent notification: UNNotification) async
|
|
331
|
+
-> UNNotificationPresentationOptions {
|
|
332
|
+
[.banner, .sound, .badge]
|
|
330
333
|
}
|
|
331
334
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
336
|
+
didReceive response: UNNotificationResponse) async {
|
|
337
|
+
let action = response.actionIdentifier
|
|
338
|
+
let threadID = response.notification.request.content.userInfo["threadID"] as? String
|
|
339
|
+
let typed = (response as? UNTextInputNotificationResponse)?.userText
|
|
340
|
+
await route(action: action, threadID: threadID, typed: typed)
|
|
341
|
+
}
|
|
339
342
|
|
|
340
|
-
|
|
343
|
+
private func route(action: String, threadID: String?, typed: String?) async {
|
|
344
|
+
switch action {
|
|
341
345
|
case UNNotificationDefaultActionIdentifier:
|
|
342
|
-
//
|
|
343
|
-
await handleNotificationTap(userInfo: userInfo)
|
|
346
|
+
AppRouter.shared.openThread(threadID) // tapped the notification
|
|
344
347
|
case UNNotificationDismissActionIdentifier:
|
|
345
|
-
//
|
|
346
|
-
break
|
|
348
|
+
break // swiped away
|
|
347
349
|
default:
|
|
348
|
-
|
|
349
|
-
await handleCustomAction(actionIdentifier, userInfo: userInfo)
|
|
350
|
+
await handleChatAction(action, threadID: threadID, typed: typed)
|
|
350
351
|
}
|
|
351
352
|
}
|
|
352
353
|
}
|
|
353
354
|
```
|
|
354
355
|
|
|
355
|
-
|
|
356
|
+
The system does not promise to call these methods on the main thread, and
|
|
357
|
+
`UNNotification` and `UNNotificationResponse` are not `Sendable`. Mark the
|
|
358
|
+
protocol methods `nonisolated`, copy the plain values you need out of the
|
|
359
|
+
response (all payload data sits in `content.userInfo`), then hop to the main
|
|
360
|
+
actor.
|
|
356
361
|
|
|
357
|
-
|
|
362
|
+
Leave out `willPresent` and anything that arrives while your app is frontmost
|
|
363
|
+
is dropped without a banner.
|
|
358
364
|
|
|
359
|
-
|
|
360
|
-
@Observable @MainActor
|
|
361
|
-
final class DeepLinkRouter {
|
|
362
|
-
static let shared = DeepLinkRouter()
|
|
365
|
+
### Deep links from a tap
|
|
363
366
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
guard let id = userInfo["messageId"] as? String else { return }
|
|
370
|
-
DeepLinkRouter.shared.pendingDestination = .chat(id: id)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// In SwiftUI -- observe and consume:
|
|
374
|
-
.onChange(of: router.pendingDestination) { _, destination in
|
|
375
|
-
if let destination {
|
|
376
|
-
path.append(destination)
|
|
377
|
-
router.pendingDestination = nil
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
```
|
|
367
|
+
Keep a single `@Observable @MainActor` router with an optional
|
|
368
|
+
`pendingDestination`. The delegate sets it; the root view watches it with
|
|
369
|
+
`.onChange(of:)`, pushes the destination onto its navigation path and then sets
|
|
370
|
+
it back to `nil`. A version that also switches tabs lives in the
|
|
371
|
+
[patterns reference](references/notification-patterns.md#routing-a-tap-to-a-screen).
|
|
381
372
|
|
|
382
|
-
|
|
373
|
+
## Actions and Categories
|
|
383
374
|
|
|
384
|
-
|
|
375
|
+
Register categories at launch, before any notification can arrive.
|
|
385
376
|
|
|
386
|
-
|
|
377
|
+
```swift
|
|
378
|
+
let reply = UNTextInputNotificationAction(
|
|
379
|
+
identifier: "REPLY",
|
|
380
|
+
title: "Reply",
|
|
381
|
+
options: [],
|
|
382
|
+
textInputButtonTitle: "Send",
|
|
383
|
+
textInputPlaceholder: "Message"
|
|
384
|
+
)
|
|
385
|
+
let archive = UNNotificationAction(identifier: "ARCHIVE", title: "Archive", options: [.destructive])
|
|
386
|
+
let open = UNNotificationAction(identifier: "OPEN", title: "Open", options: [.foreground])
|
|
387
|
+
let approve = UNNotificationAction(identifier: "APPROVE", title: "Approve", options: [.authenticationRequired])
|
|
387
388
|
|
|
388
|
-
|
|
389
|
+
let chat = UNNotificationCategory(
|
|
390
|
+
identifier: "CHAT",
|
|
391
|
+
actions: [reply, archive],
|
|
392
|
+
intentIdentifiers: [],
|
|
393
|
+
options: [.customDismissAction] // dismissal also reaches didReceive
|
|
394
|
+
)
|
|
395
|
+
let request = UNNotificationCategory(identifier: "EXPENSE", actions: [approve, open], intentIdentifiers: [])
|
|
389
396
|
|
|
390
|
-
|
|
391
|
-
func registerNotificationCategories() {
|
|
392
|
-
let replyAction = UNTextInputNotificationAction(
|
|
393
|
-
identifier: "REPLY_ACTION",
|
|
394
|
-
title: "Reply",
|
|
395
|
-
options: [],
|
|
396
|
-
textInputButtonTitle: "Send",
|
|
397
|
-
textInputPlaceholder: "Type a reply..."
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
let likeAction = UNNotificationAction(
|
|
401
|
-
identifier: "LIKE_ACTION",
|
|
402
|
-
title: "Like",
|
|
403
|
-
options: []
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
let deleteAction = UNNotificationAction(
|
|
407
|
-
identifier: "DELETE_ACTION",
|
|
408
|
-
title: "Delete",
|
|
409
|
-
options: [.destructive, .authenticationRequired]
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
let messageCategory = UNNotificationCategory(
|
|
413
|
-
identifier: "MESSAGE_CATEGORY",
|
|
414
|
-
actions: [replyAction, likeAction, deleteAction],
|
|
415
|
-
intentIdentifiers: [],
|
|
416
|
-
options: [.customDismissAction] // fires didReceive on dismiss too
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
|
|
420
|
-
}
|
|
397
|
+
UNUserNotificationCenter.current().setNotificationCategories([chat, request])
|
|
421
398
|
```
|
|
422
399
|
|
|
423
|
-
|
|
400
|
+
| Option | Effect |
|
|
401
|
+
|--------|--------|
|
|
402
|
+
| `.authenticationRequired` | Runs only after the device is unlocked |
|
|
403
|
+
| `.destructive` | Shown in red; for delete or remove |
|
|
404
|
+
| `.foreground` | Tapping it opens the app |
|
|
405
|
+
|
|
406
|
+
A text action's response arrives as a `UNTextInputNotificationResponse`; its
|
|
407
|
+
`userText` is what the delegate above passes on as `typed`:
|
|
424
408
|
|
|
425
409
|
```swift
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
guard let messageId = userInfo["messageId"] as? String else { return }
|
|
436
|
-
await MessageService.shared.deleteMessage(id: messageId)
|
|
410
|
+
@MainActor
|
|
411
|
+
func handleChatAction(_ action: String, threadID: String?, typed: String?) async {
|
|
412
|
+
guard let threadID else { return }
|
|
413
|
+
switch action {
|
|
414
|
+
case "REPLY":
|
|
415
|
+
guard let typed, !typed.isEmpty else { return }
|
|
416
|
+
await ChatService.shared.send(typed, to: threadID)
|
|
417
|
+
case "ARCHIVE":
|
|
418
|
+
await ChatService.shared.archive(threadID)
|
|
437
419
|
default:
|
|
438
420
|
break
|
|
439
421
|
}
|
|
440
422
|
}
|
|
441
423
|
```
|
|
442
424
|
|
|
443
|
-
|
|
444
|
-
- `.authenticationRequired` -- device must be unlocked to perform the action
|
|
445
|
-
- `.destructive` -- displayed in red; use for delete/remove actions
|
|
446
|
-
- `.foreground` -- launches the app to the foreground when tapped
|
|
447
|
-
|
|
448
|
-
## Notification Grouping
|
|
425
|
+
## Grouping
|
|
449
426
|
|
|
450
|
-
|
|
427
|
+
Notifications with the same `threadIdentifier` (local) or `thread-id` (APNs)
|
|
428
|
+
stack into one group; each distinct value is its own group. `summaryArgument`
|
|
429
|
+
and `summaryArgumentCount` on the content feed the group's summary line, and a
|
|
430
|
+
category can shape that line:
|
|
451
431
|
|
|
452
432
|
```swift
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
Customize the summary format string in the category:
|
|
459
|
-
|
|
460
|
-
```swift
|
|
461
|
-
let category = UNNotificationCategory(
|
|
462
|
-
identifier: "MESSAGE_CATEGORY",
|
|
463
|
-
actions: [replyAction],
|
|
433
|
+
let chat = UNNotificationCategory(
|
|
434
|
+
identifier: "CHAT",
|
|
435
|
+
actions: [],
|
|
464
436
|
intentIdentifiers: [],
|
|
465
|
-
|
|
437
|
+
hiddenPreviewsBodyPlaceholder: nil,
|
|
438
|
+
categorySummaryFormat: "%u new messages from %@",
|
|
466
439
|
options: []
|
|
467
440
|
)
|
|
468
441
|
```
|
|
469
442
|
|
|
470
443
|
## Common Mistakes
|
|
471
444
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
**DO:** Set delegate in `App.init`; use `UIApplicationDelegateAdaptor` for APNs.
|
|
484
|
-
**DON'T:** Upload APNs tokens only when they "change" or assume a fixed token length. **DO:** Upload on every `didRegister` callback and treat the token as opaque data converted to hex.
|
|
485
|
-
**DON'T:** Put Live Activity, VoIP, or App Clip-specific notification rules here. **DO:** Route those to `activitykit`, `callkit`, and `app-clips`.
|
|
445
|
+
| Mistake | Instead |
|
|
446
|
+
|---------|---------|
|
|
447
|
+
| Calling `registerForRemoteNotifications()` only after `.authorized` | Register anyway; silent pushes and server token binding need it |
|
|
448
|
+
| `String(data: token, encoding: .utf8)` | Hex-encode each byte |
|
|
449
|
+
| Promising frequent silent refreshes | Expect a few per hour at most |
|
|
450
|
+
| Expecting a silent push to run the service extension | Only `mutable-content: 1` alert pushes do |
|
|
451
|
+
| A service-extension path that never calls the handler | Call it once on every path, timeout included |
|
|
452
|
+
| No `willPresent` | Implement it or foreground notifications vanish |
|
|
453
|
+
| Setting the delegate late, or registering from a view without the adaptor | Set it in `App.init`, use `@UIApplicationDelegateAdaptor` |
|
|
454
|
+
| Uploading the token only when it "changes" | Treat it as opaque hex and upload on every callback |
|
|
455
|
+
| Mixing in rules for Live Activities, VoIP calls or App Clips | Hand those to `live-activities`, `callkit-voip`, `app-clips` |
|
|
486
456
|
|
|
487
457
|
## Review Checklist
|
|
488
458
|
|
|
489
|
-
- [ ]
|
|
490
|
-
- [ ] APNs registration not
|
|
491
|
-
- [ ]
|
|
492
|
-
- [ ]
|
|
493
|
-
- [ ]
|
|
494
|
-
- [ ] Categories
|
|
495
|
-
- [ ] Silent
|
|
496
|
-
|
|
497
|
-
##
|
|
498
|
-
|
|
499
|
-
- [
|
|
500
|
-
-
|
|
459
|
+
- [ ] Permission is requested before any alert, sound or badge, and a denied user gets a link to Settings
|
|
460
|
+
- [ ] APNs registration does not depend on alert authorization
|
|
461
|
+
- [ ] Token is hex-encoded, uploaded on every callback, not cached as truth, no fixed length
|
|
462
|
+
- [ ] The center delegate is installed from `App.init` or `application(_:didFinishLaunchingWithOptions:)`
|
|
463
|
+
- [ ] Both `willPresent` and `didReceive` are implemented
|
|
464
|
+
- [ ] Categories and actions are registered at launch when notifications are interactive
|
|
465
|
+
- [ ] Silent pushes carry only `content-available: 1` (nothing that alerts), use the `background` push type at priority 5, rely on the Remote notifications mode, accept throttling, and report an honest `UIBackgroundFetchResult`
|
|
466
|
+
|
|
467
|
+
## Apple Documentation
|
|
468
|
+
|
|
469
|
+
- [APNs registration](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns)
|
|
470
|
+
- [Requesting permission](https://developer.apple.com/documentation/usernotifications/asking-permission-to-use-notifications)
|
|
471
|
+
- [Building a remote payload](https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification)
|
|
472
|
+
- [Background updates by push](https://developer.apple.com/documentation/usernotifications/pushing-background-updates-to-your-app)
|