@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.
Files changed (264) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +266 -267
  5. package/package.json +2 -2
  6. package/pipeline/scripts/_notices.mjs +1 -1
  7. package/pipeline/skills/.skill-manifest.json +68 -68
  8. package/pipeline/skills/shared/README.md +70 -70
  9. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  10. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  11. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  12. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  13. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  14. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  15. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  16. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  17. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  18. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  19. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  20. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  21. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  22. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  23. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  24. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  25. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  26. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  27. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  28. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  29. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  30. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  31. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  32. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  34. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  35. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  36. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  37. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  38. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  39. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  40. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  41. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  42. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  43. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  44. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  45. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  46. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  47. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  48. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  49. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  50. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  51. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  52. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  53. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  54. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  55. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  56. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  57. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  58. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  59. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  60. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  61. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  62. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  63. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  64. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  65. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  66. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  67. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  68. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  69. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  70. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  71. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  72. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  73. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  74. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  75. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  76. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  77. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  78. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  79. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  80. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  81. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  82. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  83. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  84. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  85. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  86. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  87. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  88. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  89. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  90. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  91. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  92. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  93. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  94. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  95. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  96. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  97. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  98. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  99. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  100. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  101. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  102. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  103. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  104. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  105. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  106. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  107. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  108. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  109. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  111. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  112. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  113. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  114. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  115. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  116. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  117. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  118. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  119. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  120. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  121. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  122. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  123. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  124. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  125. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  126. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  127. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  128. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  129. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  130. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  131. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  132. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  133. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  134. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  135. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  136. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  137. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  138. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  139. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  140. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  141. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  142. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  143. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  144. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  145. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  146. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  147. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  148. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  149. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  150. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  151. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  152. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  153. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  154. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  155. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  156. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  157. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  158. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  159. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  160. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  161. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  162. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  163. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  164. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  165. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  166. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  167. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  168. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  169. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  170. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  171. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  172. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  173. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  174. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  175. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  176. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  177. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  178. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  179. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  180. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  181. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  182. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  183. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  184. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  185. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  186. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  187. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  188. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  189. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  190. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  191. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  192. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  193. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  194. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  195. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  196. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  197. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  198. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  199. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  200. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  201. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  202. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  203. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  204. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  205. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  206. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  207. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  208. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  209. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  210. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  211. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  212. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  213. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  214. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  215. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  216. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  217. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  218. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  219. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  220. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  221. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  222. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  223. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  224. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  225. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  226. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  227. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  228. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  229. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  230. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  231. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  232. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  233. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  234. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  235. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  236. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  237. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  238. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  239. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  240. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  241. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  242. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  243. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  244. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  245. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  246. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  247. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  248. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  249. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  250. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  251. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  252. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  253. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  254. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  255. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  256. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  257. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  258. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  259. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  260. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  261. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  262. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  263. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  264. 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: "Implement, review, or debug push notifications in iOS/macOS apps - local notifications, remote (APNs) notifications, rich notifications, notification actions, silent pushes, and notification service/content extensions. Use when working with UNUserNotificationCenter, registering for remote notifications, handling notification payloads, setting up notification categories and actions, creating rich notification content, or debugging notification delivery. Also use when working with alerts, badges, sounds, background pushes, or user notification permissions in Swift apps."
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: "dpearson2699/swift-ios-skills (PolyForm Perimeter 1.0.0)"
5
+ source: multi-agent-pipeline
6
6
  ---
7
7
 
8
8
  # Push Notifications
9
9
 
10
- Implement, review, and debug local and remote notifications on iOS/macOS using `UserNotifications` and APNs. Covers permission flow, token registration, payload structure, foreground handling, notification actions, grouping, and rich notifications. Targets iOS 26+ with Swift 6.3, backward-compatible to iOS 16 unless noted.
11
-
12
- Keep adjacent domains separate: Live Activity `content-state` payloads belong in `activitykit`; PushKit/VoIP call pushes belong in `callkit`; App Clip ephemeral notification setup belongs in `app-clips`; long-running or scheduled background work after a silent push belongs in `background-processing`.
13
-
14
- ## Contents
15
-
16
- - [Correction Reviews](#correction-reviews)
17
- - [Permission Flow](#permission-flow)
18
- - [APNs Registration](#apns-registration)
19
- - [Local Notifications](#local-notifications)
20
- - [Remote Notification Payload](#remote-notification-payload)
21
- - [Notification Handling](#notification-handling)
22
- - [Notification Actions and Categories](#notification-actions-and-categories)
23
- - [Notification Grouping](#notification-grouping)
24
- - [Common Mistakes](#common-mistakes)
25
- - [Review Checklist](#review-checklist)
26
- - [References](#references)
27
-
28
- ## Correction Reviews
29
-
30
- When reviewing flawed notification proposals, explicitly name the violated contract. APNs token reviews must say token registration is independent from alert authorization, upload on every `didRegister` callback, avoid local-cache-as-truth logic, never assume token length, and treat Simulator registration failure as expected while noting `.apns` files or `simctl push` can simulate delivery. Background-push reviews must say `content-available` only, `apns-push-type: background`, `apns-priority: 5`, Remote notifications background mode, low priority, throttled, not guaranteed, not every few minutes, and bounded `didReceiveRemoteNotification` returning the correct `UIBackgroundFetchResult`. Rich-notification reviews must say service extensions require `mutable-content: 1` plus an alert payload, silent pushes do not trigger them, attachments are supported on-disk files that the system validates and stores, secrets use Keychain Sharing while App Groups are for shared files/UserDefaults, communication notifications require capability + `NSUserActivityTypes` + `INInteraction` donation + `content.updating(from:)`, and every service-extension path including attachment/download failures and `serviceExtensionTimeWillExpire()` must call the content handler exactly once with original, best-attempt, or updated content.
31
-
32
- ## Permission Flow
33
-
34
- Request notification authorization before scheduling or displaying user-visible alerts, sounds, or badges. The system prompt appears only once; subsequent calls return the stored decision. APNs token registration is separate: call `registerForRemoteNotifications()` when the app needs a device token, even if the user hasn't granted alert authorization.
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 requestNotificationPermission() async -> Bool {
41
- let center = UNUserNotificationCenter.current()
82
+ func askForAlertPermission() async -> Bool {
42
83
  do {
43
- let granted = try await center.requestAuthorization(
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
- ### Checking Current Status
92
+ ### Reading the current setting
55
93
 
56
- Always check status before assuming permissions. The user can change settings at any time.
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
- @MainActor
60
- func checkNotificationStatus() async -> UNAuthorizationStatus {
61
- let settings = await UNUserNotificationCenter.current().notificationSettings()
62
- return settings.authorizationStatus
63
- // .notDetermined, .denied, .authorized, .provisional, .ephemeral
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 Notifications
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
- ```swift
72
- // Delivers silently -- no permission prompt shown to the user
73
- try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])
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 Alerts
117
+ ### Critical alerts
77
118
 
78
- Critical alerts bypass Do Not Disturb and the mute switch. Requires a special entitlement from Apple (request via developer portal). Use only for health, safety, or security scenarios.
79
-
80
- ```swift
81
- // Requires com.apple.developer.usernotifications.critical-alerts entitlement
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
- ### Handling Denied Permissions
124
+ ### When the user said no
88
125
 
89
- When the user has denied notifications, guide them to Settings with `UIApplication.openSettingsURLString`. Do not repeatedly prompt or nag.
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 Registration
129
+ ## Registering with APNs
92
130
 
93
- Use `UIApplicationDelegateAdaptor` to receive the device token in a SwiftUI app. The AppDelegate callbacks are the only way to receive APNs tokens.
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
- @main
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 launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
139
+ didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
111
140
  ) -> Bool {
112
- UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
141
+ UNUserNotificationCenter.current().delegate = InboxNotificationDelegate.shared
113
142
  return true
114
143
  }
115
144
 
116
- func application(
117
- _ application: UIApplication,
118
- didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
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
- _ application: UIApplication,
128
- didFailToRegisterForRemoteNotificationsWithError error: Error
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
- ### Registration Order
158
+ ### Order of calls
137
159
 
138
- Configure delegates and categories at launch. Then request user-notification authorization in context for visible notifications, and register with APNs whenever the app needs a device token. Do not gate APNs registration on `.authorized`; without alert authorization, remote notifications are delivered silently.
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 configureNotifications() async {
170
+ func prepareNotifications() async {
143
171
  let center = UNUserNotificationCenter.current()
144
- let settings = await center.notificationSettings()
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 Handling
179
+ ### Token rules
156
180
 
157
- Device tokens change. Re-send the token to your server every time `didRegisterForRemoteNotificationsWithDeviceToken` fires, not just the first time. Do not persist tokens locally as a source of truth or assume a fixed token length.
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
- Schedule notifications directly from the device without a server. Useful for reminders, timers, and location-based alerts.
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 = "Workout Reminder"
168
- content.subtitle = "Time to move"
169
- content.body = "You have a scheduled workout in 15 minutes."
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 = ["workoutId": "abc123"]
173
- content.threadIdentifier = "workouts" // groups in notification center
198
+ content.userInfo = ["plantID": "basil-01"]
199
+ content.threadIdentifier = "garden" // groups these together in Notification Center
174
200
  ```
175
201
 
176
- ### Trigger Types
202
+ ### Triggers
177
203
 
178
204
  ```swift
179
- // Fire after a time interval (minimum 60 seconds for repeating)
180
- let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)
181
-
182
- // Fire at a specific date/time
183
- var dateComponents = DateComponents()
184
- dateComponents.hour = 8
185
- dateComponents.minute = 30
186
- let calendarTrigger = UNCalendarNotificationTrigger(
187
- dateMatching: dateComponents, repeats: true // daily at 8:30 AM
188
- )
189
-
190
- // Fire when entering a geographic region
191
- let region = CLCircularRegion(
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
- region.notifyOnEntry = true
197
- region.notifyOnExit = false
198
- let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)
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 Managing
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
- // Remove specific pending notifications
215
- center.removePendingNotificationRequests(withIdentifiers: ["workout-reminder-abc123"])
232
+ let waiting = await center.pendingNotificationRequests()
216
233
 
217
- // Remove all pending
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 Notification Payload
240
+ ## Remote Payloads
229
241
 
230
- ### Standard APNs Payload
242
+ ### Alert push
231
243
 
232
244
  ```json
233
245
  {
234
- "aps": {
235
- "alert": {
236
- "title": "New Message",
237
- "subtitle": "From Alice",
238
- "body": "Hey, are you free for lunch?"
239
- },
240
- "badge": 3,
241
- "sound": "default",
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
- ### Silent / Background Push
257
+ App-specific keys go beside `aps`, never inside it.
251
258
 
252
- Set `content-available: 1` with no alert, sound, or badge. Requires "Background Modes > Remote notifications" plus APNs headers `apns-push-type: background` and `apns-priority: 5`. The system treats these as low priority, throttled, and not guaranteed; do not send them every few minutes or rely on them for immediate freshness. In `didReceiveRemoteNotification`, do bounded work and return a `UIBackgroundFetchResult` promptly, within the background execution window.
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
- Handle in AppDelegate:
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
- _ application: UIApplication,
267
- didReceiveRemoteNotification userInfo: [AnyHashable: Any]
268
- ) async -> UIBackgroundFetchResult {
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 DataSyncService.shared.sync(trigger: updateType)
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 Content
286
+ ### Mutable content
282
287
 
283
- Set `mutable-content: 1` plus an `alert` dictionary to let a Notification Service Extension modify an alerting remote notification before display. Silent pushes do not trigger the service extension. Use service extensions for bounded work such as downloading supported on-disk attachments, decrypting display text, or configuring communication notifications; call the content handler on every success, failure, and timeout path. For communication notifications, enable the capability, add `NSUserActivityTypes`, donate the `INInteraction`, then call `content.updating(from:)`.
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
- "aps": {
288
- "alert": { "title": "Photo", "body": "Alice sent a photo" },
289
- "mutable-content": 1
290
- },
291
- "imageUrl": "https://example.com/photo.jpg"
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
- ### Localized Notifications
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
- Use localization keys so the notification displays in the user's language:
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
- ## Notification Handling
317
+ ## Handling Notifications
312
318
 
313
- ### UNUserNotificationCenterDelegate
319
+ ### The delegate
314
320
 
315
- Implement the delegate to control foreground display and handle user taps. Set the delegate as early as possible -- in `application(_:didFinishLaunchingWithOptions:)` or `App.init`.
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 NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
320
- static let shared = NotificationDelegate()
321
-
322
- // Called when notification arrives while app is in FOREGROUND
323
- func userNotificationCenter(
324
- _ center: UNUserNotificationCenter,
325
- willPresent notification: UNNotification
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
- // Called when user TAPS the notification
333
- func userNotificationCenter(
334
- _ center: UNUserNotificationCenter,
335
- didReceive response: UNNotificationResponse
336
- ) async {
337
- let userInfo = response.notification.request.content.userInfo
338
- let actionIdentifier = response.actionIdentifier
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
- switch actionIdentifier {
343
+ private func route(action: String, threadID: String?, typed: String?) async {
344
+ switch action {
341
345
  case UNNotificationDefaultActionIdentifier:
342
- // User tapped the notification body
343
- await handleNotificationTap(userInfo: userInfo)
346
+ AppRouter.shared.openThread(threadID) // tapped the notification
344
347
  case UNNotificationDismissActionIdentifier:
345
- // User dismissed the notification
346
- break
348
+ break // swiped away
347
349
  default:
348
- // Custom action button tapped
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
- ### Deep Linking from Notifications
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
- Route notification taps to the correct screen using a shared `@Observable` router. The delegate writes a pending destination; the SwiftUI view observes and consumes it.
362
+ Leave out `willPresent` and anything that arrives while your app is frontmost
363
+ is dropped without a banner.
358
364
 
359
- ```swift
360
- @Observable @MainActor
361
- final class DeepLinkRouter {
362
- static let shared = DeepLinkRouter()
365
+ ### Deep links from a tap
363
366
 
364
- var pendingDestination: AppDestination?
365
- }
366
-
367
- // In NotificationDelegate:
368
- func handleNotificationTap(userInfo: [AnyHashable: Any]) async {
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
- See [references/notification-patterns.md](references/notification-patterns.md) for the full deep-linking handler with tab switching.
373
+ ## Actions and Categories
383
374
 
384
- ## Notification Actions and Categories
375
+ Register categories at launch, before any notification can arrive.
385
376
 
386
- Define interactive actions that appear as buttons on the notification. Register categories at launch.
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
- ### Defining Categories and Actions
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
- ```swift
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
- ### Handling Action Responses
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
- func handleCustomAction(_ identifier: String, userInfo: [AnyHashable: Any]) async {
427
- switch identifier {
428
- case "REPLY_ACTION":
429
- // response is UNTextInputNotificationResponse for text input actions
430
- break
431
- case "LIKE_ACTION":
432
- guard let messageId = userInfo["messageId"] as? String else { return }
433
- await MessageService.shared.likeMessage(id: messageId)
434
- case "DELETE_ACTION":
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
- Action options:
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
- Group related notifications with `threadIdentifier` (or `thread-id` in the APNs payload). Each unique thread becomes a separate group in Notification Center.
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
- content.threadIdentifier = "chat-alice" // all messages from Alice group together
454
- content.summaryArgument = "Alice"
455
- content.summaryArgumentCount = 3 // "3 more notifications from Alice"
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
- categorySummaryFormat: "%u more messages from %@",
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
- **DON'T:** Gate APNs token registration on alert authorization when the app needs silent pushes or server token binding.
473
- **DO:** Request authorization for alerts/sounds/badges, and register with APNs whenever a device token is needed.
474
- **DON'T:** Convert device token with `String(data: deviceToken, encoding: .utf8)`.
475
- **DO:** Use hex: `deviceToken.map { String(format: "%02x", $0) }.joined()`.
476
- **DON'T:** Promise every-few-minutes silent refresh or immediate background delivery.
477
- **DO:** Say background pushes are low priority, throttled, not guaranteed, limited to a few per hour in practice, and require bounded `didReceiveRemoteNotification` work that returns the correct `UIBackgroundFetchResult`.
478
- **DON'T:** Expect a silent push to run a Notification Service Extension, or leave the extension without calling its content handler.
479
- **DO:** Use `mutable-content: 1` with an alert payload, supported on-disk attachments that the system validates and stores, `INInteraction` donation plus `content.updating(from:)` for communication notifications, and original or best-attempt content on every success, failure, and timeout path.
480
- **DON'T:** Forget foreground handling. Without `willPresent`, notifications are silently suppressed.
481
- **DO:** Implement `willPresent` and return `.banner`, `.sound`, `.badge`.
482
- **DON'T:** Set delegate too late or register from SwiftUI views without AppDelegate adaptor.
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
- - [ ] Authorization requested before visible alerts/sounds/badges; denied case handled (Settings link)
490
- - [ ] APNs registration not incorrectly blocked by alert authorization status
491
- - [ ] Device token converted to hex, uploaded on every callback, and not treated as a locally cached or fixed-length constant
492
- - [ ] `UNUserNotificationCenterDelegate` set in `App.init` or `application(_:didFinishLaunching:)`
493
- - [ ] Foreground (`willPresent`) and tap (`didReceive`) handling implemented
494
- - [ ] Categories/actions registered at launch if interactive notifications needed
495
- - [ ] Silent push uses `content-available: 1`, no alert/sound/badge, `apns-push-type: background`, `apns-priority: 5`, Background Modes > Remote notifications, throttling caveats, and correct `UIBackgroundFetchResult`
496
-
497
- ## References
498
- - [references/notification-patterns.md](references/notification-patterns.md) - AppDelegate setup, APNs callbacks, deep-link router, silent push, debugging
499
- - [references/rich-notifications.md](references/rich-notifications.md) - Service Extension, Content Extension, attachments, communication notifications
500
- - Apple docs: [APNs registration](https://sosumi.ai/documentation/usernotifications/registering-your-app-with-apns), [permission](https://sosumi.ai/documentation/usernotifications/asking-permission-to-use-notifications), [payloads](https://sosumi.ai/documentation/usernotifications/generating-a-remote-notification), [background pushes](https://sosumi.ai/documentation/usernotifications/pushing-background-updates-to-your-app)
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)