@mmerterden/multi-agent-pipeline 20.6.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 +27 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +328 -329
- package/package.json +2 -2
- package/pipeline/multi-agent-refs/features/design-conformance.md +62 -64
- package/pipeline/scripts/_notices.mjs +12 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +106 -106
- package/pipeline/skills/shared/README.md +71 -71
- package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +1 -0
- 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/android-architecture/SKILL.md +2 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +2 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +2 -0
- 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/backlog/BACKLOG.md +1 -1
- package/pipeline/skills/shared/external/backlog/SKILL.md +56 -33
- 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/ci-cd-pipelines/SKILL.md +1 -0
- 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/compose-components/SKILL.md +2 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +3 -2
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +2 -0
- 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/council/SKILL.md +1 -0
- 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/css-modern/SKILL.md +1 -0
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -0
- 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/evidence-github/SKILL.md +2 -0
- package/pipeline/skills/shared/external/evidence-registry/SKILL.md +2 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +2 -0
- 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/html-semantic/SKILL.md +1 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -0
- 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-coding-standard/SKILL.md +1 -0
- 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-module-structure/SKILL.md +1 -0
- 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-security/SKILL.md +2 -0
- 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/localization-reuse-map/SKILL.md +91 -283
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +119 -151
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +60 -90
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +119 -156
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +726 -787
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +253 -288
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +243 -304
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +88 -104
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +181 -235
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +198 -263
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +461 -466
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +145 -151
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +123 -141
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +146 -157
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +22 -19
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +156 -140
- 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/nextjs-app-router/SKILL.md +1 -0
- 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/play-store-review/SKILL.md +2 -0
- 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/python-patterns/SKILL.md +1 -0
- package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -0
- 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/rest-api-design/SKILL.md +1 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +2 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +2 -0
- package/pipeline/skills/shared/external/search-first/SKILL.md +1 -0
- 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/signal-community/SKILL.md +2 -0
- package/pipeline/skills/shared/external/skill-creator/SKILL.md +80 -41
- package/pipeline/skills/shared/external/skill-creator/audit.md +63 -59
- package/pipeline/skills/shared/external/skill-creator/checklist.md +28 -20
- package/pipeline/skills/shared/external/skill-creator/examples.md +40 -40
- package/pipeline/skills/shared/external/skill-creator/label-check.md +48 -36
- package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +91 -100
- package/pipeline/skills/shared/external/skill-creator/template.md +51 -39
- 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/tailwind-css/SKILL.md +1 -0
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -0
- 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/typescript-patterns/SKILL.md +1 -0
- 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/vue-composition/SKILL.md +1 -0
- 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/web-accessibility/SKILL.md +1 -0
- package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -0
- package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -0
- 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
package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md
CHANGED
|
@@ -1,218 +1,173 @@
|
|
|
1
1
|
# Notification Patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Working code for the app side of notifications: wiring at launch, the center
|
|
4
|
+
delegate, routing a tap to a screen, silent pushes, a small scheduler, the
|
|
5
|
+
token lifecycle, category setup, testing, badges and moving a provisional user
|
|
6
|
+
to full permission.
|
|
4
7
|
|
|
5
8
|
## Contents
|
|
6
9
|
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [Silent Push
|
|
11
|
-
- [
|
|
12
|
-
- [Token
|
|
13
|
-
- [
|
|
14
|
-
- [Testing
|
|
15
|
-
- [
|
|
16
|
-
- [Provisional to Full
|
|
10
|
+
- [App and AppDelegate Wiring](#app-and-appdelegate-wiring)
|
|
11
|
+
- [The Center Delegate](#the-center-delegate)
|
|
12
|
+
- [Routing a Tap to a Screen](#routing-a-tap-to-a-screen)
|
|
13
|
+
- [Handling a Silent Push](#handling-a-silent-push)
|
|
14
|
+
- [A Local Scheduler](#a-local-scheduler)
|
|
15
|
+
- [Token Lifecycle](#token-lifecycle)
|
|
16
|
+
- [Registering Categories](#registering-categories)
|
|
17
|
+
- [Testing and Debugging Delivery](#testing-and-debugging-delivery)
|
|
18
|
+
- [Badges](#badges)
|
|
19
|
+
- [From Provisional to Full Permission](#from-provisional-to-full-permission)
|
|
17
20
|
|
|
18
|
-
##
|
|
21
|
+
## App and AppDelegate Wiring
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
SwiftUI calls the app's `init` ahead of the delegate's launch callback
|
|
24
|
+
(`application(_:didFinishLaunchingWithOptions:)`), so the center delegate is
|
|
25
|
+
installed there, as early as it can be.
|
|
21
26
|
|
|
22
27
|
```swift
|
|
23
|
-
import SwiftUI
|
|
24
|
-
import UserNotifications
|
|
25
|
-
|
|
26
28
|
@main
|
|
27
|
-
struct
|
|
28
|
-
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
29
|
-
@State private var router =
|
|
29
|
+
struct GardenApp: App {
|
|
30
|
+
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
|
31
|
+
@State private var router = NotificationRouter()
|
|
30
32
|
|
|
31
33
|
init() {
|
|
32
|
-
|
|
33
|
-
// App.init runs before application(_:didFinishLaunchingWithOptions:).
|
|
34
|
-
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
|
34
|
+
UNUserNotificationCenter.current().delegate = GardenNotificationDelegate.shared
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
var body: some Scene {
|
|
38
38
|
WindowGroup {
|
|
39
|
-
|
|
39
|
+
RootView()
|
|
40
40
|
.environment(router)
|
|
41
|
-
.task { await
|
|
41
|
+
.task { await startNotifications() }
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
@MainActor
|
|
46
|
+
private func startNotifications() async {
|
|
47
|
+
GardenNotificationDelegate.shared.router = router
|
|
48
|
+
NotificationCategories.registerAll()
|
|
48
49
|
let center = UNUserNotificationCenter.current()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if settings.authorizationStatus == .notDetermined {
|
|
52
|
-
_ = try? await center.requestAuthorization(
|
|
53
|
-
options: [.alert, .sound, .badge]
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// APNs registration is independent from alert authorization. If the
|
|
58
|
-
// user denies alerts, remote notifications can still arrive silently.
|
|
59
|
-
await MainActor.run {
|
|
60
|
-
UIApplication.shared.registerForRemoteNotifications()
|
|
50
|
+
if await center.notificationSettings().authorizationStatus == .notDetermined {
|
|
51
|
+
_ = try? await center.requestAuthorization(options: [.badge, .alert, .sound])
|
|
61
52
|
}
|
|
53
|
+
// Register whatever the answer was: a user who declined alerts
|
|
54
|
+
// still receives remote notifications, only silently.
|
|
55
|
+
UIApplication.shared.registerForRemoteNotifications()
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
58
|
```
|
|
65
59
|
|
|
66
|
-
### AppDelegate
|
|
60
|
+
### The AppDelegate
|
|
67
61
|
|
|
68
62
|
```swift
|
|
69
|
-
class AppDelegate: NSObject, UIApplicationDelegate {
|
|
70
|
-
func application(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Delegate may also be set here if not set in App.init.
|
|
75
|
-
// Setting in App.init is preferred because it runs earlier.
|
|
76
|
-
return true
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
func application(
|
|
80
|
-
_ application: UIApplication,
|
|
81
|
-
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
|
82
|
-
) {
|
|
83
|
-
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
84
|
-
print("APNs device token: \(token)")
|
|
85
|
-
|
|
86
|
-
// Always send token to server -- tokens can change between launches.
|
|
63
|
+
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
64
|
+
func application(_ application: UIApplication,
|
|
65
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
66
|
+
let hex = deviceToken.reduce(into: "") { $0 += String(format: "%02x", $1) }
|
|
67
|
+
// Upload on every call: the token may differ from the last launch.
|
|
87
68
|
Task { @MainActor in
|
|
88
|
-
await
|
|
69
|
+
await PushTokenService.shared.upload(hex)
|
|
89
70
|
}
|
|
90
71
|
}
|
|
91
72
|
|
|
92
|
-
func application(
|
|
93
|
-
|
|
94
|
-
didFailToRegisterForRemoteNotificationsWithError error: Error
|
|
95
|
-
) {
|
|
73
|
+
func application(_ application: UIApplication,
|
|
74
|
+
didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
96
75
|
#if targetEnvironment(simulator)
|
|
97
|
-
//
|
|
98
|
-
// does not register with APNs for a real device token.
|
|
99
|
-
print("Simulator: APNs device-token registration is unavailable.")
|
|
76
|
+
// Always happens here; use .apns files or simctl push to test.
|
|
100
77
|
#else
|
|
101
|
-
|
|
102
|
-
// Log to your analytics/crash reporting service
|
|
78
|
+
Diagnostics.record("apns_registration_failed", error: error)
|
|
103
79
|
#endif
|
|
104
80
|
}
|
|
105
81
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
) async -> UIBackgroundFetchResult {
|
|
111
|
-
await BackgroundNotificationHandler.shared.handle(userInfo: userInfo)
|
|
82
|
+
func application(_ application: UIApplication,
|
|
83
|
+
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async
|
|
84
|
+
-> UIBackgroundFetchResult {
|
|
85
|
+
await SilentPushHandler().handle(job: userInfo["job"] as? String)
|
|
112
86
|
}
|
|
113
87
|
}
|
|
114
88
|
```
|
|
115
89
|
|
|
116
|
-
##
|
|
90
|
+
## The Center Delegate
|
|
91
|
+
|
|
92
|
+
The protocol methods are `nonisolated`: the system may call them off the main
|
|
93
|
+
thread and the notification objects are not `Sendable`. Each one copies the
|
|
94
|
+
plain values it needs, then hands them to main-actor code.
|
|
117
95
|
|
|
118
96
|
```swift
|
|
97
|
+
struct TapSummary: Sendable {
|
|
98
|
+
let category: String
|
|
99
|
+
let action: String
|
|
100
|
+
let threadID: String?
|
|
101
|
+
let plantID: String?
|
|
102
|
+
let typed: String?
|
|
103
|
+
|
|
104
|
+
init(_ response: UNNotificationResponse) {
|
|
105
|
+
let content = response.notification.request.content
|
|
106
|
+
category = content.categoryIdentifier
|
|
107
|
+
action = response.actionIdentifier
|
|
108
|
+
threadID = content.userInfo["threadID"] as? String
|
|
109
|
+
plantID = content.userInfo["plantID"] as? String
|
|
110
|
+
typed = (response as? UNTextInputNotificationResponse)?.userText
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
119
114
|
@MainActor
|
|
120
|
-
final class
|
|
121
|
-
static let shared =
|
|
115
|
+
final class GardenNotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|
116
|
+
static let shared = GardenNotificationDelegate()
|
|
117
|
+
weak var router: NotificationRouter?
|
|
118
|
+
var visibleThreadID: String?
|
|
122
119
|
|
|
123
120
|
private override init() { super.init() }
|
|
124
121
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let userInfo = notification.request.content.userInfo
|
|
133
|
-
|
|
134
|
-
// Example: suppress banner for the chat screen the user is already viewing
|
|
135
|
-
if let chatId = userInfo["chatId"] as? String,
|
|
136
|
-
NavigationState.shared.currentChatId == chatId {
|
|
137
|
-
// Still update badge/list but do not show a banner
|
|
122
|
+
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
123
|
+
willPresent notification: UNNotification) async
|
|
124
|
+
-> UNNotificationPresentationOptions {
|
|
125
|
+
let thread = notification.request.content.userInfo["threadID"] as? String
|
|
126
|
+
let onScreen = await visibleThreadID
|
|
127
|
+
if let thread, thread == onScreen {
|
|
128
|
+
// The user is already looking at this conversation: skip the banner.
|
|
138
129
|
return [.badge, .list, .sound]
|
|
139
130
|
}
|
|
140
|
-
|
|
141
131
|
return [.banner, .sound, .badge, .list]
|
|
142
132
|
}
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
) async {
|
|
149
|
-
let userInfo = response.notification.request.content.userInfo
|
|
150
|
-
let categoryIdentifier = response.notification.request.content.categoryIdentifier
|
|
134
|
+
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
135
|
+
didReceive response: UNNotificationResponse) async {
|
|
136
|
+
await handle(TapSummary(response))
|
|
137
|
+
}
|
|
151
138
|
|
|
152
|
-
|
|
139
|
+
private func handle(_ tap: TapSummary) async {
|
|
140
|
+
switch tap.action {
|
|
153
141
|
case UNNotificationDefaultActionIdentifier:
|
|
154
|
-
|
|
155
|
-
|
|
142
|
+
openScreen(for: tap)
|
|
156
143
|
case UNNotificationDismissActionIdentifier:
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
break
|
|
160
|
-
|
|
144
|
+
// Only delivered when the category has .customDismissAction.
|
|
145
|
+
await ChatService.shared.markSeen(tap.threadID)
|
|
161
146
|
default:
|
|
162
|
-
|
|
163
|
-
await handleAction(
|
|
164
|
-
response.actionIdentifier,
|
|
165
|
-
category: categoryIdentifier,
|
|
166
|
-
response: response,
|
|
167
|
-
userInfo: userInfo
|
|
168
|
-
)
|
|
147
|
+
await perform(tap)
|
|
169
148
|
}
|
|
170
149
|
}
|
|
171
150
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
switch category {
|
|
179
|
-
case "MESSAGE_CATEGORY":
|
|
180
|
-
guard let chatId = userInfo["chatId"] as? String else { return }
|
|
181
|
-
DeepLinkRouter.shared.navigate(to: .chat(id: chatId))
|
|
182
|
-
|
|
183
|
-
case "WORKOUT_CATEGORY":
|
|
184
|
-
guard let workoutId = userInfo["workoutId"] as? String else { return }
|
|
185
|
-
DeepLinkRouter.shared.navigate(to: .workout(id: workoutId))
|
|
186
|
-
|
|
151
|
+
private func openScreen(for tap: TapSummary) {
|
|
152
|
+
switch tap.category {
|
|
153
|
+
case "CHAT":
|
|
154
|
+
if let id = tap.threadID { router?.navigate(to: .thread(id: id)) }
|
|
155
|
+
case "WATERING":
|
|
156
|
+
if let id = tap.plantID { router?.navigate(to: .plant(id: id)) }
|
|
187
157
|
default:
|
|
188
|
-
//
|
|
189
|
-
break
|
|
158
|
+
break // unknown category: the app simply opens where it was
|
|
190
159
|
}
|
|
191
160
|
}
|
|
192
161
|
|
|
193
|
-
private func
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
case ("
|
|
201
|
-
|
|
202
|
-
let chatId = userInfo["chatId"] as? String else { return }
|
|
203
|
-
await MessageService.shared.sendReply(
|
|
204
|
-
text: textResponse.userText,
|
|
205
|
-
chatId: chatId
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
case ("MESSAGE_CATEGORY", "LIKE_ACTION"):
|
|
209
|
-
guard let messageId = userInfo["messageId"] as? String else { return }
|
|
210
|
-
await MessageService.shared.likeMessage(id: messageId)
|
|
211
|
-
|
|
212
|
-
case ("MESSAGE_CATEGORY", "MARK_READ_ACTION"):
|
|
213
|
-
guard let chatId = userInfo["chatId"] as? String else { return }
|
|
214
|
-
await MessageService.shared.markAsRead(chatId: chatId)
|
|
215
|
-
|
|
162
|
+
private func perform(_ tap: TapSummary) async {
|
|
163
|
+
switch (tap.category, tap.action) {
|
|
164
|
+
case ("CHAT", "REPLY"):
|
|
165
|
+
guard let text = tap.typed, let id = tap.threadID else { return }
|
|
166
|
+
await ChatService.shared.send(text, to: id)
|
|
167
|
+
case ("CHAT", "MARK_READ"):
|
|
168
|
+
await ChatService.shared.markSeen(tap.threadID)
|
|
169
|
+
case ("WATERING", "DONE"):
|
|
170
|
+
if let id = tap.plantID { await PlantStore.shared.logWatering(id) }
|
|
216
171
|
default:
|
|
217
172
|
break
|
|
218
173
|
}
|
|
@@ -220,16 +175,16 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|
|
220
175
|
}
|
|
221
176
|
```
|
|
222
177
|
|
|
223
|
-
##
|
|
224
|
-
|
|
225
|
-
An `@Observable` router that bridges notification taps to SwiftUI navigation. The router holds a pending destination that the view layer observes and consumes.
|
|
178
|
+
## Routing a Tap to a Screen
|
|
226
179
|
|
|
227
180
|
```swift
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
181
|
+
enum AppDestination: Hashable {
|
|
182
|
+
case thread(id: String)
|
|
183
|
+
case plant(id: String)
|
|
184
|
+
}
|
|
232
185
|
|
|
186
|
+
@Observable @MainActor
|
|
187
|
+
final class NotificationRouter {
|
|
233
188
|
var pendingDestination: AppDestination?
|
|
234
189
|
|
|
235
190
|
func navigate(to destination: AppDestination) {
|
|
@@ -237,200 +192,134 @@ final class DeepLinkRouter {
|
|
|
237
192
|
}
|
|
238
193
|
}
|
|
239
194
|
|
|
240
|
-
enum
|
|
241
|
-
case chat(id: String)
|
|
242
|
-
case workout(id: String)
|
|
243
|
-
case profile(userId: String)
|
|
244
|
-
case settings
|
|
245
|
-
}
|
|
195
|
+
enum AppTab: Hashable { case chats, garden }
|
|
246
196
|
|
|
247
|
-
// In the root view:
|
|
248
197
|
struct RootView: View {
|
|
249
|
-
@Environment(
|
|
250
|
-
@State private var
|
|
251
|
-
@State private var
|
|
252
|
-
@State private var
|
|
198
|
+
@Environment(NotificationRouter.self) private var router
|
|
199
|
+
@State private var tab: AppTab = .chats
|
|
200
|
+
@State private var chatsPath = NavigationPath()
|
|
201
|
+
@State private var gardenPath = NavigationPath()
|
|
253
202
|
|
|
254
203
|
var body: some View {
|
|
255
|
-
TabView(selection: $
|
|
256
|
-
Tab("
|
|
257
|
-
NavigationStack(path: $
|
|
258
|
-
|
|
259
|
-
.navigationDestination(for: AppDestination.self) {
|
|
260
|
-
destinationView(for: dest)
|
|
261
|
-
}
|
|
204
|
+
TabView(selection: $tab) {
|
|
205
|
+
Tab("Chats", systemImage: "bubble.left.and.bubble.right", value: AppTab.chats) {
|
|
206
|
+
NavigationStack(path: $chatsPath) {
|
|
207
|
+
ChatListView()
|
|
208
|
+
.navigationDestination(for: AppDestination.self) { DestinationView($0) }
|
|
262
209
|
}
|
|
263
210
|
}
|
|
264
|
-
Tab("
|
|
265
|
-
NavigationStack(path: $
|
|
266
|
-
|
|
267
|
-
.navigationDestination(for: AppDestination.self) {
|
|
268
|
-
destinationView(for: dest)
|
|
269
|
-
}
|
|
211
|
+
Tab("Garden", systemImage: "leaf", value: AppTab.garden) {
|
|
212
|
+
NavigationStack(path: $gardenPath) {
|
|
213
|
+
GardenView()
|
|
214
|
+
.navigationDestination(for: AppDestination.self) { DestinationView($0) }
|
|
270
215
|
}
|
|
271
216
|
}
|
|
272
217
|
}
|
|
273
|
-
.onChange(of: router.pendingDestination) { _,
|
|
274
|
-
guard let
|
|
275
|
-
|
|
218
|
+
.onChange(of: router.pendingDestination) { _, target in
|
|
219
|
+
guard let target else { return }
|
|
220
|
+
switch target {
|
|
221
|
+
case .thread:
|
|
222
|
+
tab = .chats
|
|
223
|
+
chatsPath = NavigationPath()
|
|
224
|
+
chatsPath.append(target)
|
|
225
|
+
case .plant:
|
|
226
|
+
tab = .garden
|
|
227
|
+
gardenPath = NavigationPath()
|
|
228
|
+
gardenPath.append(target)
|
|
229
|
+
}
|
|
276
230
|
router.pendingDestination = nil
|
|
277
231
|
}
|
|
278
232
|
}
|
|
279
|
-
|
|
280
|
-
private func handleDeepLink(_ destination: AppDestination) {
|
|
281
|
-
switch destination {
|
|
282
|
-
case .chat:
|
|
283
|
-
selectedTab = .chat
|
|
284
|
-
chatPath = NavigationPath()
|
|
285
|
-
chatPath.append(destination)
|
|
286
|
-
case .workout:
|
|
287
|
-
selectedTab = .home
|
|
288
|
-
homePath = NavigationPath()
|
|
289
|
-
homePath.append(destination)
|
|
290
|
-
case .profile, .settings:
|
|
291
|
-
selectedTab = .home
|
|
292
|
-
homePath = NavigationPath()
|
|
293
|
-
homePath.append(destination)
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
@ViewBuilder
|
|
298
|
-
private func destinationView(for destination: AppDestination) -> some View {
|
|
299
|
-
switch destination {
|
|
300
|
-
case .chat(let id): ChatDetailView(chatId: id)
|
|
301
|
-
case .workout(let id): WorkoutDetailView(workoutId: id)
|
|
302
|
-
case .profile(let userId): ProfileView(userId: userId)
|
|
303
|
-
case .settings: SettingsView()
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
233
|
}
|
|
307
234
|
```
|
|
308
235
|
|
|
309
|
-
|
|
236
|
+
`Tab(_:systemImage:value:)` needs iOS 18; on older targets use `.tabItem` and
|
|
237
|
+
`.tag`.
|
|
310
238
|
|
|
311
|
-
|
|
239
|
+
## Handling a Silent Push
|
|
312
240
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
func handle(userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
|
|
319
|
-
guard let action = userInfo["action"] as? String else {
|
|
320
|
-
return .noData
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
do {
|
|
324
|
-
switch action {
|
|
325
|
-
case "sync":
|
|
326
|
-
let hasNewData = try await DataSyncService.shared.performSync()
|
|
327
|
-
return hasNewData ? .newData : .noData
|
|
328
|
-
|
|
329
|
-
case "refresh-config":
|
|
330
|
-
try await ConfigService.shared.refreshRemoteConfig()
|
|
331
|
-
return .newData
|
|
332
|
-
|
|
333
|
-
case "clear-cache":
|
|
334
|
-
await CacheManager.shared.clearExpired()
|
|
335
|
-
return .newData
|
|
241
|
+
The app gets roughly 30 seconds of background time. Report the real outcome:
|
|
242
|
+
the system uses the results to learn when your data changes and to plan later
|
|
243
|
+
wake-ups. The delegate reads the `job` string out of `userInfo` before calling
|
|
244
|
+
the handler: the dictionary is not `Sendable`, so Swift 6 rejects handing it
|
|
245
|
+
from the main-actor delegate to a nonisolated async method.
|
|
336
246
|
|
|
337
|
-
|
|
338
|
-
|
|
247
|
+
```swift
|
|
248
|
+
struct SilentPushHandler {
|
|
249
|
+
func handle(job: String?) async -> UIBackgroundFetchResult {
|
|
250
|
+
switch job {
|
|
251
|
+
case "refreshInbox":
|
|
252
|
+
do {
|
|
253
|
+
let changed = try await MailboxSync.shared.refresh()
|
|
254
|
+
return changed ? .newData : .noData
|
|
255
|
+
} catch {
|
|
256
|
+
return .failed
|
|
339
257
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
return .
|
|
258
|
+
case "purgeCache":
|
|
259
|
+
await ImageCache.shared.purgeExpired()
|
|
260
|
+
return .noData
|
|
261
|
+
default:
|
|
262
|
+
return .noData
|
|
343
263
|
}
|
|
344
264
|
}
|
|
345
265
|
}
|
|
346
266
|
```
|
|
347
267
|
|
|
348
|
-
|
|
268
|
+
Apple's guidance caps background pushes at roughly two or three per hour. Do not use them to poll a server, and never make something the user can
|
|
269
|
+
see depend on one arriving.
|
|
349
270
|
|
|
350
|
-
When reviewing a
|
|
271
|
+
When reviewing a silent-push design, flag each of these if missing:
|
|
272
|
+
- It does not deliver instant freshness.
|
|
273
|
+
- APNs can throttle or drop the push.
|
|
274
|
+
- The Remote notifications background mode is on.
|
|
275
|
+
- The handler finishes quickly and its result matches what happened.
|
|
351
276
|
|
|
352
|
-
##
|
|
277
|
+
## A Local Scheduler
|
|
353
278
|
|
|
354
|
-
|
|
279
|
+
Wrapping the center in one type keeps call sites small and gives tests a seam.
|
|
355
280
|
|
|
356
281
|
```swift
|
|
357
282
|
@MainActor
|
|
358
283
|
final class NotificationScheduler {
|
|
359
|
-
private let center
|
|
360
|
-
|
|
361
|
-
// MARK: - Schedule
|
|
362
|
-
|
|
363
|
-
func scheduleReminder(
|
|
364
|
-
id: String,
|
|
365
|
-
title: String,
|
|
366
|
-
body: String,
|
|
367
|
-
at date: Date,
|
|
368
|
-
repeats: Bool = false,
|
|
369
|
-
categoryIdentifier: String? = nil,
|
|
370
|
-
userInfo: [String: String] = [:]
|
|
371
|
-
) async throws {
|
|
372
|
-
let content = UNMutableNotificationContent()
|
|
373
|
-
content.title = title
|
|
374
|
-
content.body = body
|
|
375
|
-
content.sound = .default
|
|
376
|
-
content.userInfo = userInfo
|
|
377
|
-
if let category = categoryIdentifier {
|
|
378
|
-
content.categoryIdentifier = category
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
let components = Calendar.current.dateComponents(
|
|
382
|
-
[.year, .month, .day, .hour, .minute],
|
|
383
|
-
from: date
|
|
384
|
-
)
|
|
385
|
-
let trigger = UNCalendarNotificationTrigger(
|
|
386
|
-
dateMatching: components,
|
|
387
|
-
repeats: repeats
|
|
388
|
-
)
|
|
284
|
+
private let center: UNUserNotificationCenter
|
|
389
285
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
content: content,
|
|
393
|
-
trigger: trigger
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
try await center.add(request)
|
|
286
|
+
init(center: UNUserNotificationCenter = .current()) {
|
|
287
|
+
self.center = center
|
|
397
288
|
}
|
|
398
289
|
|
|
399
|
-
func
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
hour:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
let content = UNMutableNotificationContent()
|
|
407
|
-
content.title = title
|
|
408
|
-
content.body = body
|
|
409
|
-
content.sound = .default
|
|
410
|
-
|
|
411
|
-
var components = DateComponents()
|
|
412
|
-
components.hour = hour
|
|
413
|
-
components.minute = minute
|
|
414
|
-
|
|
415
|
-
let trigger = UNCalendarNotificationTrigger(
|
|
416
|
-
dateMatching: components,
|
|
417
|
-
repeats: true
|
|
418
|
-
)
|
|
290
|
+
func remind(_ key: String, headline: String, text: String, on moment: Date,
|
|
291
|
+
categoryID: String? = nil) async throws {
|
|
292
|
+
let payload = makeContent(headline, text)
|
|
293
|
+
if let categoryID { payload.categoryIdentifier = categoryID }
|
|
294
|
+
let when = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: moment)
|
|
295
|
+
try await enqueue(key, payload, UNCalendarNotificationTrigger(dateMatching: when, repeats: false))
|
|
296
|
+
}
|
|
419
297
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
298
|
+
func remindEveryDay(_ key: String, headline: String, text: String,
|
|
299
|
+
atHour hour: Int, minute: Int) async throws {
|
|
300
|
+
var when = DateComponents()
|
|
301
|
+
when.hour = hour
|
|
302
|
+
when.minute = minute
|
|
303
|
+
let payload = makeContent(headline, text)
|
|
304
|
+
try await enqueue(key, payload, UNCalendarNotificationTrigger(dateMatching: when, repeats: true))
|
|
305
|
+
}
|
|
425
306
|
|
|
426
|
-
|
|
307
|
+
private func makeContent(_ headline: String, _ text: String) -> UNMutableNotificationContent {
|
|
308
|
+
let payload = UNMutableNotificationContent()
|
|
309
|
+
payload.title = headline
|
|
310
|
+
payload.body = text
|
|
311
|
+
payload.sound = .default
|
|
312
|
+
return payload
|
|
427
313
|
}
|
|
428
314
|
|
|
429
|
-
|
|
315
|
+
private func enqueue(_ key: String, _ payload: UNNotificationContent,
|
|
316
|
+
_ trigger: UNNotificationTrigger) async throws {
|
|
317
|
+
try await center.add(UNNotificationRequest(identifier: key, content: payload, trigger: trigger))
|
|
318
|
+
}
|
|
430
319
|
|
|
431
|
-
func
|
|
432
|
-
center.removePendingNotificationRequests(withIdentifiers: [
|
|
433
|
-
center.removeDeliveredNotifications(withIdentifiers: [
|
|
320
|
+
func cancel(_ key: String) {
|
|
321
|
+
center.removePendingNotificationRequests(withIdentifiers: [key])
|
|
322
|
+
center.removeDeliveredNotifications(withIdentifiers: [key])
|
|
434
323
|
}
|
|
435
324
|
|
|
436
325
|
func cancelAll() {
|
|
@@ -438,229 +327,183 @@ final class NotificationScheduler {
|
|
|
438
327
|
center.removeAllDeliveredNotifications()
|
|
439
328
|
}
|
|
440
329
|
|
|
441
|
-
func
|
|
442
|
-
await center.pendingNotificationRequests()
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
func isScheduled(id: String) async -> Bool {
|
|
446
|
-
let pending = await center.pendingNotificationRequests()
|
|
447
|
-
return pending.contains { $0.identifier == id }
|
|
330
|
+
func hasPending(_ key: String) async -> Bool {
|
|
331
|
+
let queued = await center.pendingNotificationRequests()
|
|
332
|
+
return queued.contains { $0.identifier == key }
|
|
448
333
|
}
|
|
449
334
|
}
|
|
450
335
|
```
|
|
451
336
|
|
|
452
|
-
## Token
|
|
337
|
+
## Token Lifecycle
|
|
453
338
|
|
|
454
|
-
|
|
339
|
+
The token is short-lived data. Send whatever the latest callback delivered;
|
|
340
|
+
never check a value in `UserDefaults` to decide an upload can be skipped.
|
|
455
341
|
|
|
456
342
|
```swift
|
|
457
|
-
@MainActor
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
static let shared = TokenService()
|
|
461
|
-
|
|
343
|
+
@MainActor @Observable
|
|
344
|
+
final class PushTokenService {
|
|
345
|
+
static let shared = PushTokenService()
|
|
462
346
|
private(set) var currentToken: String?
|
|
463
347
|
|
|
464
|
-
func upload(
|
|
348
|
+
func upload(_ hexToken: String) async {
|
|
465
349
|
do {
|
|
466
|
-
try await
|
|
467
|
-
currentToken =
|
|
350
|
+
try await DeviceAPI.register(pushToken: hexToken)
|
|
351
|
+
currentToken = hexToken
|
|
468
352
|
} catch {
|
|
469
|
-
|
|
470
|
-
// Retry on next launch -- didRegisterForRemoteNotifications fires again
|
|
353
|
+
// Nothing to retry by hand: the next launch delivers the token again.
|
|
471
354
|
}
|
|
472
355
|
}
|
|
473
356
|
|
|
474
|
-
/// Call when the user logs out to disassociate the token from their account.
|
|
475
357
|
func invalidate() async {
|
|
476
|
-
|
|
477
|
-
|
|
358
|
+
if let active = currentToken {
|
|
359
|
+
try? await DeviceAPI.unregister(pushToken: active)
|
|
360
|
+
}
|
|
478
361
|
currentToken = nil
|
|
479
362
|
}
|
|
480
363
|
}
|
|
481
364
|
```
|
|
482
365
|
|
|
483
|
-
|
|
366
|
+
Call `invalidate()` at sign-out. Without it the server keeps the token bound to
|
|
367
|
+
the old account and that account's pushes keep landing on this device.
|
|
484
368
|
|
|
485
|
-
##
|
|
369
|
+
## Registering Categories
|
|
486
370
|
|
|
487
|
-
Register
|
|
371
|
+
Register at launch, before the first notification can arrive.
|
|
372
|
+
`setNotificationCategories` replaces the whole set, so pass every category in
|
|
373
|
+
one call.
|
|
488
374
|
|
|
489
375
|
```swift
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
identifier: "START_WORKOUT_ACTION",
|
|
515
|
-
title: "Start",
|
|
516
|
-
options: [.foreground]
|
|
517
|
-
)
|
|
518
|
-
let skipAction = UNNotificationAction(
|
|
519
|
-
identifier: "SKIP_WORKOUT_ACTION",
|
|
520
|
-
title: "Skip",
|
|
521
|
-
options: [.destructive]
|
|
522
|
-
)
|
|
523
|
-
let workoutCategory = UNNotificationCategory(
|
|
524
|
-
identifier: "WORKOUT_CATEGORY",
|
|
525
|
-
actions: [startAction, skipAction],
|
|
526
|
-
intentIdentifiers: [],
|
|
527
|
-
options: []
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
UNUserNotificationCenter.current().setNotificationCategories([
|
|
531
|
-
messageCategory,
|
|
532
|
-
workoutCategory,
|
|
533
|
-
])
|
|
376
|
+
enum NotificationCategories {
|
|
377
|
+
static func registerAll() {
|
|
378
|
+
let reply = UNTextInputNotificationAction(
|
|
379
|
+
identifier: "REPLY", title: "Reply", options: [],
|
|
380
|
+
textInputButtonTitle: "Send", textInputPlaceholder: "Write a reply"
|
|
381
|
+
)
|
|
382
|
+
let markRead = UNNotificationAction(identifier: "MARK_READ", title: "Mark as Read", options: [])
|
|
383
|
+
let chat = UNNotificationCategory(
|
|
384
|
+
identifier: "CHAT",
|
|
385
|
+
actions: [reply, markRead],
|
|
386
|
+
intentIdentifiers: [],
|
|
387
|
+
hiddenPreviewsBodyPlaceholder: nil,
|
|
388
|
+
categorySummaryFormat: "%u more from %@",
|
|
389
|
+
options: [.customDismissAction]
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
let done = UNNotificationAction(identifier: "DONE", title: "Watered", options: [.foreground])
|
|
393
|
+
let skip = UNNotificationAction(identifier: "SKIP", title: "Skip Today", options: [.destructive])
|
|
394
|
+
let watering = UNNotificationCategory(
|
|
395
|
+
identifier: "WATERING", actions: [done, skip], intentIdentifiers: []
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
UNUserNotificationCenter.current().setNotificationCategories([chat, watering])
|
|
399
|
+
}
|
|
534
400
|
}
|
|
535
401
|
```
|
|
536
402
|
|
|
537
|
-
## Testing
|
|
403
|
+
## Testing and Debugging Delivery
|
|
538
404
|
|
|
539
|
-
### Simulator
|
|
405
|
+
### What Simulator can and cannot do
|
|
540
406
|
|
|
541
|
-
-
|
|
542
|
-
|
|
543
|
-
-
|
|
544
|
-
- `
|
|
407
|
+
- It never receives pushes from APNs, and
|
|
408
|
+
`didFailToRegisterForRemoteNotificationsWithError` fires every time.
|
|
409
|
+
- Local notifications behave normally.
|
|
410
|
+
- A `.apns` file dropped onto the Simulator window is shown as a remote
|
|
411
|
+
notification.
|
|
545
412
|
|
|
546
|
-
### Simulating
|
|
413
|
+
### Simulating a remote notification
|
|
547
414
|
|
|
548
|
-
|
|
415
|
+
The file needs a top-level `Simulator Target Bundle` key alongside `aps` and
|
|
416
|
+
any custom keys:
|
|
549
417
|
|
|
550
418
|
```json
|
|
551
419
|
{
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
"title": "Test Notification",
|
|
556
|
-
"body": "This is a simulated push notification."
|
|
557
|
-
},
|
|
558
|
-
"badge": 1,
|
|
559
|
-
"sound": "default",
|
|
560
|
-
"category": "MESSAGE_CATEGORY"
|
|
561
|
-
},
|
|
562
|
-
"messageId": "test-123",
|
|
563
|
-
"chatId": "chat-abc"
|
|
420
|
+
"Simulator Target Bundle": "org.sample.garden",
|
|
421
|
+
"aps": { "alert": { "title": "Basil", "body": "Time to water." }, "sound": "default" },
|
|
422
|
+
"plantID": "basil-01"
|
|
564
423
|
}
|
|
565
424
|
```
|
|
566
425
|
|
|
567
|
-
Or use the `xcrun simctl push` command:
|
|
568
|
-
|
|
569
426
|
```bash
|
|
570
|
-
xcrun simctl push booted
|
|
427
|
+
xcrun simctl push booted org.sample.garden watering.apns
|
|
571
428
|
```
|
|
572
429
|
|
|
573
|
-
###
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
|
590
|
-
|
|
591
|
-
|
|
|
592
|
-
|
|
|
593
|
-
|
|
|
594
|
-
|
|
|
595
|
-
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
##
|
|
614
|
-
|
|
615
|
-
|
|
430
|
+
### On a device
|
|
431
|
+
|
|
432
|
+
Debug builds receive sandbox tokens; release and TestFlight builds talk to the
|
|
433
|
+
production APNs endpoint. The Push Notifications Console in the developer
|
|
434
|
+
account sends test pushes to development tokens and shows delivery logs; a
|
|
435
|
+
request generated there, or your own script, can also be replayed.
|
|
436
|
+
|
|
437
|
+
### When nothing arrives
|
|
438
|
+
|
|
439
|
+
- The provisioning profile includes the push entitlement and the app carries
|
|
440
|
+
the `aps-environment` key.
|
|
441
|
+
- The provider sends the token as hex, with no assumption about its size.
|
|
442
|
+
|
|
443
|
+
| APNs status | Meaning |
|
|
444
|
+
|-------------|---------|
|
|
445
|
+
| 200 | Accepted |
|
|
446
|
+
| 400 | Bad request; check the payload format (4096 bytes at most) |
|
|
447
|
+
| 403 | Certificate or signing key is wrong or has expired |
|
|
448
|
+
| 405 | Wrong method; requests must be POST over HTTP/2 |
|
|
449
|
+
| 410 | Token is no longer valid; delete it on the server |
|
|
450
|
+
| 413 | Payload over 4096 bytes |
|
|
451
|
+
| 429 | Too many requests; back off |
|
|
452
|
+
| 500 | APNs internal error; retry later |
|
|
453
|
+
|
|
454
|
+
On a Mac with the device attached, Console.app filtered to `apsd`, `dasd` or
|
|
455
|
+
the app's bundle ID shows the system's side of delivery.
|
|
456
|
+
|
|
457
|
+
Frequent causes of missing notifications:
|
|
458
|
+
- Low Power Mode is deferring work.
|
|
459
|
+
- A Focus or Do Not Disturb mode does not allow the app.
|
|
460
|
+
- No network.
|
|
461
|
+
- A stale token (410).
|
|
462
|
+
- Silent pushes above the few-per-hour budget.
|
|
463
|
+
- A payload over 4096 bytes.
|
|
464
|
+
- An expired APNs certificate or key.
|
|
465
|
+
|
|
466
|
+
Silent pushes are handled more generously while debugging than in production,
|
|
467
|
+
where delays are normal. For background work that must run, use
|
|
468
|
+
`BGTaskScheduler` (see `background-processing`).
|
|
469
|
+
|
|
470
|
+
## Badges
|
|
471
|
+
|
|
472
|
+
Clear the badge when the app becomes active so an old count does not stay on
|
|
473
|
+
the icon. `setBadgeCount(_:)` is async and throws, available from iOS 16; add
|
|
474
|
+
an availability check when the deployment target is older. The two-parameter
|
|
475
|
+
`onChange(of:)` closure below is iOS 17; on iOS 16 use the single-value form.
|
|
616
476
|
|
|
617
477
|
```swift
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
var body: some View {
|
|
628
|
-
MainView()
|
|
629
|
-
.onChange(of: scenePhase) { _, phase in
|
|
630
|
-
if phase == .active {
|
|
631
|
-
Task {
|
|
632
|
-
try? await UNUserNotificationCenter.current().setBadgeCount(0)
|
|
633
|
-
}
|
|
634
|
-
}
|
|
478
|
+
struct BadgeResetModifier: ViewModifier {
|
|
479
|
+
@Environment(\.scenePhase) private var phase
|
|
480
|
+
|
|
481
|
+
func body(content wrapped: Content) -> some View {
|
|
482
|
+
wrapped.onChange(of: phase) { _, newPhase in
|
|
483
|
+
guard newPhase == .active else { return }
|
|
484
|
+
Task {
|
|
485
|
+
let center = UNUserNotificationCenter.current()
|
|
486
|
+
try? await center.setBadgeCount(0)
|
|
635
487
|
}
|
|
488
|
+
}
|
|
636
489
|
}
|
|
637
490
|
}
|
|
638
491
|
```
|
|
639
492
|
|
|
640
|
-
|
|
493
|
+
A UIKit scene can do the same in `sceneDidBecomeActive(_:)`.
|
|
641
494
|
|
|
642
|
-
## Provisional to Full
|
|
495
|
+
## From Provisional to Full Permission
|
|
643
496
|
|
|
644
|
-
|
|
497
|
+
A provisional user can be asked again: requesting `[.alert, .sound, .badge]`
|
|
498
|
+
now shows the real system prompt.
|
|
645
499
|
|
|
646
500
|
```swift
|
|
647
501
|
@MainActor
|
|
648
|
-
func
|
|
502
|
+
func upgradeFromProvisional() async -> Bool {
|
|
649
503
|
let center = UNUserNotificationCenter.current()
|
|
650
|
-
let
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
do {
|
|
657
|
-
// This presents the system prompt to the user
|
|
658
|
-
let granted = try await center.requestAuthorization(
|
|
659
|
-
options: [.alert, .sound, .badge]
|
|
660
|
-
)
|
|
661
|
-
return granted
|
|
662
|
-
} catch {
|
|
663
|
-
return false
|
|
664
|
-
}
|
|
504
|
+
let status = await center.notificationSettings().authorizationStatus
|
|
505
|
+
guard status == .provisional else { return status == .authorized }
|
|
506
|
+
let granted = try? await center.requestAuthorization(options: [.sound, .badge, .alert])
|
|
507
|
+
return granted ?? false
|
|
665
508
|
}
|
|
666
509
|
```
|