@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,262 +1,236 @@
1
1
  # Rich Notifications
2
2
 
3
- Rich notifications enhance the standard notification banner with images, video, audio, custom UI, and interactive elements. They use two extension types: Notification Service Extension (modifies content before display) and Notification Content Extension (provides custom UI in the expanded notification).
3
+ Two app extensions shape how a notification looks:
4
+
5
+ - **Notification Service Extension**: edits the content of a remote
6
+ notification before the user sees it.
7
+ - **Notification Content Extension**: draws a custom interface when the user
8
+ expands the notification.
4
9
 
5
10
  ## Contents
6
11
 
7
- - [Notification Service Extension](#notification-service-extension)
8
- - [Notification Attachments](#notification-attachments)
9
- - [Notification Content Extension](#notification-content-extension)
12
+ - [Service Extension](#service-extension)
13
+ - [Attachments](#attachments)
14
+ - [Content Extension](#content-extension)
10
15
  - [Communication Notifications](#communication-notifications)
11
- - [Extension Gotchas](#extension-gotchas)
12
- - [Complete Service Extension Example](#complete-service-extension-example)
16
+ - [Troubleshooting Extensions](#troubleshooting-extensions)
17
+ - [A Complete Service Extension](#a-complete-service-extension)
18
+
19
+ ## Service Extension
20
+
21
+ The extension is launched only for a remote notification that alerts the user
22
+ (its `alert` has a title, subtitle or body) and carries `mutable-content: 1`.
23
+ It then has about 30 seconds.
13
24
 
14
- ## Notification Service Extension
25
+ When a design gets this wrong, correct these four points:
26
+ 1. A silent push never launches the extension.
27
+ 2. Attachments are supported files on disk; the system validates them and
28
+ moves them into its own storage.
29
+ 3. A communication notification needs the capability, an
30
+ `NSUserActivityTypes` entry, a donated `INInteraction`, and a call to
31
+ `content.updating(from:)`.
32
+ 4. The content handler runs exactly once on every path: success, partial
33
+ failure, bad payload and `serviceExtensionTimeWillExpire()`.
15
34
 
16
- A Notification Service Extension runs for an alerting remote notification whose payload has `mutable-content: 1` and an `alert` dictionary with title, subtitle, or body content. It has approximately 30 seconds to modify the notification content before the system displays it. Call the content handler on every path: success, partial failure, invalid payload, and `serviceExtensionTimeWillExpire()`. If the extension does not call the handler in time, the system displays the original notification.
35
+ If the handler is never called in time, the user sees the original,
36
+ unmodified notification.
17
37
 
18
- When reviewing a flawed rich-notification design, explicitly correct four contracts: silent pushes do not trigger service extensions; attachments must be supported files on disk and are validated and stored by the system; communication notifications require the capability, `NSUserActivityTypes`, `INInteraction` donation, and `content.updating(from:)`; every service-extension path, including download/decryption failures and `serviceExtensionTimeWillExpire()`, must call the content handler exactly once with original, best-attempt, or updated content.
38
+ ### Creating it
19
39
 
20
- ### Creating the Extension
40
+ Add a target in Xcode (File > New > Target) and pick the Notification Service
41
+ Extension template. It adds a `NotificationService` class that subclasses
42
+ `UNNotificationServiceExtension`.
21
43
 
22
- In Xcode: File > New > Target > Notification Service Extension. This creates a new target with a `NotificationService` class.
44
+ The template's shape has a trap: the async work and the timeout callback can
45
+ both try to deliver. Guard delivery so only the first caller wins:
23
46
 
24
47
  ```swift
25
48
  import UserNotifications
26
49
 
27
- class NotificationService: UNNotificationServiceExtension {
28
- private var contentHandler: ((UNNotificationContent) -> Void)?
29
- private var bestAttemptContent: UNMutableNotificationContent?
30
- private var didComplete = false
50
+ final class NotificationService: UNNotificationServiceExtension, @unchecked Sendable {
51
+ private let lock = NSLock()
52
+ private var handler: ((UNNotificationContent) -> Void)?
53
+ private var draft: UNMutableNotificationContent?
31
54
 
32
55
  override func didReceive(
33
56
  _ request: UNNotificationRequest,
34
57
  withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
35
58
  ) {
36
- self.contentHandler = contentHandler
37
- bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
38
-
39
- guard let content = bestAttemptContent else {
40
- contentHandler(request.content)
59
+ let original = request.content
60
+ guard let copy = original.mutableCopy() as? UNMutableNotificationContent else {
61
+ contentHandler(original)
41
62
  return
42
63
  }
64
+ lock.withLock {
65
+ handler = contentHandler
66
+ draft = copy
67
+ }
68
+ let headline = request.content.userInfo["headline"] as? String
43
69
 
44
- // Modify the notification content here
45
70
  Task {
46
- await processNotification(content: content)
47
- contentHandler(content)
71
+ let cleaned = await HeadlineFormatter.clean(headline)
72
+ self.deliver { content in
73
+ if let cleaned { content.title = cleaned }
74
+ }
48
75
  }
49
76
  }
50
77
 
51
- /// Called if the extension is about to be terminated (ran out of time).
52
- /// Deliver the best attempt -- even a partially modified notification
53
- /// is better than nothing.
54
78
  override func serviceExtensionTimeWillExpire() {
55
- if let handler = contentHandler, let content = bestAttemptContent {
56
- handler(content)
57
- }
79
+ deliver() // hand over whatever the draft holds now
58
80
  }
59
81
 
60
- private func processNotification(content: UNMutableNotificationContent) async {
61
- // Download image if URL is provided
62
- if let imageUrlString = content.userInfo["imageUrl"] as? String {
63
- await attachImage(from: imageUrlString, to: content)
64
- }
65
-
66
- // Decrypt body if encrypted
67
- if let encrypted = content.userInfo["encryptedBody"] as? String {
68
- content.body = decrypt(encrypted)
82
+ /// Calls the content handler at most once, whichever path gets here first.
83
+ private func deliver(_ edit: (UNMutableNotificationContent) -> Void = { _ in }) {
84
+ let pending: (((UNNotificationContent) -> Void), UNMutableNotificationContent)? = lock.withLock {
85
+ guard let handler, let draft else { return nil }
86
+ self.handler = nil
87
+ self.draft = nil
88
+ return (handler, draft)
69
89
  }
90
+ guard let (deliverTo, content) = pending else { return }
91
+ edit(content)
92
+ deliverTo(content)
70
93
  }
71
94
  }
72
95
  ```
73
96
 
74
- ### Downloading Images and Media
75
-
76
- Download media from a URL and attach it to the notification. The attachment must be written to disk in the extension's temporary directory.
97
+ ### Downloading media
77
98
 
78
99
  ```swift
79
- extension NotificationService {
80
- private func attachImage(
81
- from urlString: String,
82
- to content: UNMutableNotificationContent
83
- ) async {
84
- guard let url = URL(string: urlString) else { return }
85
-
100
+ enum MediaFetcher {
101
+ static func attachment(from remote: URL) async -> UNNotificationAttachment? {
102
+ let session = URLSession.shared
103
+ guard let (bytes, reply) = try? await session.data(from: remote) else {
104
+ return nil // show the notification without media
105
+ }
106
+ let suffix = fileExtension(for: reply.mimeType)
107
+ let local = FileManager.default.temporaryDirectory
108
+ .appendingPathComponent(UUID().uuidString)
109
+ .appendingPathExtension(suffix)
86
110
  do {
87
- let (data, response) = try await URLSession.shared.data(from: url)
88
-
89
- // Determine file extension from MIME type
90
- let ext = fileExtension(for: response.mimeType)
91
- let tempDir = FileManager.default.temporaryDirectory
92
- let fileURL = tempDir.appendingPathComponent(UUID().uuidString + ext)
93
-
94
- try data.write(to: fileURL)
95
-
96
- let attachment = try UNNotificationAttachment(
97
- identifier: "image",
98
- url: fileURL,
99
- options: nil
100
- )
101
- content.attachments = [attachment]
111
+ try bytes.write(to: local)
112
+ return try UNNotificationAttachment(identifier: "media", url: local, options: nil)
102
113
  } catch {
103
- print("Failed to download notification image: \(error)")
104
- // Notification displays without the image -- graceful degradation
114
+ return nil
105
115
  }
106
116
  }
107
117
 
108
- private func fileExtension(for mimeType: String?) -> String {
109
- switch mimeType {
110
- case "image/jpeg": return ".jpg"
111
- case "image/png": return ".png"
112
- case "image/gif": return ".gif"
113
- case "video/mp4", "video/mpeg4": return ".mp4"
114
- case "audio/mpeg", "audio/mp3": return ".mp3"
115
- case "audio/wav": return ".wav"
116
- default: return ".jpg"
118
+ static func fileExtension(for mime: String?) -> String {
119
+ switch mime {
120
+ case "image/jpeg": "jpg"
121
+ case "image/png": "png"
122
+ case "image/gif": "gif"
123
+ case "video/mp4", "video/mpeg4": "mp4"
124
+ case "audio/mpeg", "audio/mp3": "mp3"
125
+ case "audio/wav", "audio/x-wav": "wav"
126
+ default: "jpg"
117
127
  }
118
128
  }
119
129
  }
120
130
  ```
121
131
 
122
- ### Decrypting Encrypted Payloads
132
+ A failed download is not an error to the user: deliver the text without media.
133
+
134
+ ### Decrypting the payload
123
135
 
124
- Use the service extension to decrypt sensitive notification content. The APNs payload carries an encrypted blob; the extension decrypts it before display.
136
+ For end-to-end encrypted messages the server sends a base64 ciphertext and the
137
+ extension decrypts it before display. If decoding or decryption fails, show
138
+ neutral text such as "New message" rather than nothing.
125
139
 
126
140
  ```swift
127
- extension NotificationService {
128
- private func decrypt(_ encryptedBase64: String) -> String {
129
- guard let data = Data(base64Encoded: encryptedBase64) else {
130
- return "New notification" // fallback
131
- }
132
- // Use your encryption library (CryptoKit, etc.) to decrypt
133
- // Store notification decryption keys in a Keychain access group shared
134
- // by the app and extension, not in UserDefaults or the APNs payload.
135
- do {
136
- let decrypted = try EncryptionService.shared.decrypt(data)
137
- return String(data: decrypted, encoding: .utf8) ?? "New notification"
138
- } catch {
139
- return "New notification"
140
- }
141
- }
141
+ if let sealed = info["sealed"] as? String,
142
+ let bytes = Data(base64Encoded: sealed),
143
+ let key = try? SharedKeychain.messageKey(),
144
+ let plain = try? MessageCrypto.open(bytes, using: key) {
145
+ content.body = plain
146
+ } else {
147
+ content.body = "New message"
142
148
  }
143
149
  ```
144
150
 
145
- ### Sharing Data with Extensions
151
+ The key belongs in a Keychain access group that the app and the extension both
152
+ use. Never put it in `UserDefaults`, and never in the push payload.
146
153
 
147
- The service extension runs in a separate process from the main app. Use App Groups for shared files and `UserDefaults`; use Keychain Sharing for secrets or tokens.
154
+ ### Sharing data with the app
148
155
 
149
- 1. Enable "App Groups" capability on both the main app target and the extension target.
150
- 2. Use the same group identifier (e.g., `group.com.example.myapp`).
151
- 3. Enable "Keychain Sharing" on both targets for shared keychain items. The `kSecAttrAccessGroup` value must be one of the target's keychain access groups, not the App Group container identifier.
156
+ The extension is a separate process.
152
157
 
153
- ```swift
154
- // Shared UserDefaults
155
- let sharedDefaults = UserDefaults(suiteName: "group.com.example.myapp")
156
-
157
- // Shared file container
158
- let sharedContainer = FileManager.default.containerURL(
159
- forSecurityApplicationGroupIdentifier: "group.com.example.myapp"
160
- )
161
-
162
- // Shared Keychain: set kSecAttrAccessGroup to a Keychain Sharing access group
163
- // that both targets include in their entitlements.
164
- ```
158
+ - **App Groups**: turn on the capability for both targets with the same
159
+ identifier (for example `group.com.example.garden`). Use
160
+ `UserDefaults(suiteName:)` for small values and
161
+ `FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:)` for
162
+ files.
163
+ - **Keychain Sharing**: turn it on for both targets. The
164
+ `kSecAttrAccessGroup` value has to appear in the target's list of keychain
165
+ access groups; the App Group identifier does not work there.
165
166
 
166
- **Extension memory:** Notification Service Extensions are memory-constrained. Do not load large frameworks or perform memory-intensive operations. If the system terminates the extension, it shows the original notification.
167
+ Service extensions get little memory. Keep dependencies small; if the system
168
+ kills the extension, the original notification is shown.
167
169
 
168
- ## Notification Attachments
170
+ ## Attachments
169
171
 
170
- `UNNotificationAttachment` supports images, GIFs, video, and audio. The system displays a thumbnail in the collapsed notification and the full media in the expanded view.
172
+ `UNNotificationAttachment` handles pictures (GIFs included), sound and movies. The collapsed
173
+ notification shows a thumbnail; the expanded one shows the media.
171
174
 
172
- ### Supported Formats and Limits
175
+ | Kind | Size limit | Formats |
176
+ |------|------------|---------|
177
+ | Image | 10 MB | JPEG, GIF, PNG |
178
+ | Audio | 5 MB | WAV, AIFF, M4A, MP3 |
179
+ | Video | 50 MB | MPEG-4, MPEG-2, MPEG, AVI |
173
180
 
174
- | Type | Formats | Max Size |
175
- |------|---------|----------|
176
- | Image | JPEG, PNG, GIF | 10 MB |
177
- | Audio | AIFF, WAV, MP3, M4A | 5 MB |
178
- | Video | MPEG, MPEG-2, MPEG-4, AVI | 50 MB |
179
-
180
- ### Creating Attachments
181
+ Thumbnail options:
181
182
 
182
183
  ```swift
183
- // From a local file URL
184
- let attachment = try UNNotificationAttachment(
185
- identifier: "photo",
186
- url: localFileURL,
187
- options: nil
188
- )
189
-
190
- // With options for thumbnailing
191
- let attachment = try UNNotificationAttachment(
192
- identifier: "photo",
193
- url: localFileURL,
194
- options: [
195
- UNNotificationAttachmentOptionsThumbnailClippingRectKey:
196
- CGRect(x: 0, y: 0, width: 1, height: 0.5).dictionaryRepresentation,
197
- UNNotificationAttachmentOptionsThumbnailTimeKey: 0 // for video: thumbnail at 0 seconds
198
- ]
199
- )
200
-
201
- // Attach to content
202
- content.attachments = [attachment]
184
+ let options: [AnyHashable: Any] = [
185
+ // Normalized rect: show the centre half of the image as the thumbnail.
186
+ UNNotificationAttachmentOptionsThumbnailClippingRectKey:
187
+ CGRect(origin: CGPoint(x: 0.25, y: 0.25), size: CGSize(width: 0.5, height: 0.5))
188
+ .dictionaryRepresentation,
189
+ // For video: the frame at 3 seconds.
190
+ UNNotificationAttachmentOptionsThumbnailTimeKey: 3
191
+ ]
203
192
  ```
204
193
 
205
- **Important:** The file URL must point to a supported audio, image, or video file on disk. For service extensions, write downloads to the extension's temporary directory before creating `UNNotificationAttachment`; do not attach arbitrary remote URLs or unsupported file types. For local notifications, create the attachment from a file the app can read when scheduling. The system validates attachments and moves them into its attachment data store; it copies attachments located inside the app bundle.
206
-
207
- ### Multiple Attachments
208
-
209
- You can attach multiple items, but only the first attachment is shown as the thumbnail in the collapsed notification. The expanded view can show all attachments.
210
-
211
- ```swift
212
- content.attachments = [imageAttachment, audioAttachment]
213
- // imageAttachment appears as the thumbnail
214
- ```
194
+ Rules:
195
+ - The URL points to a supported file on disk. A remote URL or an unsupported
196
+ type fails.
197
+ - For a local notification, the file must be readable when you schedule it.
198
+ - The system validates the file and moves it into its own data store. Files
199
+ inside the app bundle are copied instead.
200
+ - Several attachments are allowed; the first one is the thumbnail.
201
+ - GIFs can animate in the expanded view; check the real presentation on a
202
+ device.
215
203
 
216
- ### GIF Animations
204
+ ## Content Extension
217
205
 
218
- GIF files are supported image attachments and may contain an animated image sequence. Test the expanded notification UI for the actual presentation you need.
206
+ A content extension supplies a view controller that appears when the user
207
+ long-presses or expands the notification. Use it when an attachment is not
208
+ enough.
219
209
 
220
- ```swift
221
- let gifURL = tempDir.appendingPathComponent("animation.gif")
222
- try gifData.write(to: gifURL)
223
- let attachment = try UNNotificationAttachment(
224
- identifier: "animation",
225
- url: gifURL,
226
- options: nil
227
- )
228
- ```
210
+ Add it the same way (File > New > Target), choosing the Notification Content
211
+ Extension template; Xcode adds
212
+ a storyboard and a `NotificationViewController`.
229
213
 
230
- ## Notification Content Extension
231
-
232
- A Notification Content Extension provides a custom view controller that displays when the user long-presses (or expands) a notification. Use it for richer UI than attachments alone can provide.
233
-
234
- ### Creating the Extension
235
-
236
- In Xcode: File > New > Target > Notification Content Extension. This creates a new target with a storyboard and a `NotificationViewController`.
237
-
238
- ### Configuration (Info.plist)
239
-
240
- The extension's `Info.plist` must declare which notification categories it handles:
214
+ ### Info.plist
241
215
 
242
216
  ```xml
243
217
  <key>NSExtension</key>
244
218
  <dict>
245
219
  <key>NSExtensionAttributes</key>
246
220
  <dict>
221
+ <!-- One category as a string, or several as an array -->
247
222
  <key>UNNotificationExtensionCategory</key>
248
- <!-- Use a string for one category, or an array for multiple. -->
249
223
  <array>
250
- <string>MESSAGE_CATEGORY</string>
251
- <string>PHOTO_CATEGORY</string>
224
+ <string>ORDER_UPDATE</string>
225
+ <string>DELIVERY_PHOTO</string>
252
226
  </array>
253
- <!-- Optional: size ratio (height / width). Default 1.0 -->
227
+ <!-- Height as a fraction of width; 1.0 when omitted -->
254
228
  <key>UNNotificationExtensionInitialContentSizeRatio</key>
255
- <real>0.5</real>
256
- <!-- Optional: hide the default notification body below the custom UI -->
229
+ <real>0.6</real>
230
+ <!-- Hide the standard title/body under the custom view -->
257
231
  <key>UNNotificationExtensionDefaultContentHidden</key>
258
232
  <true/>
259
- <!-- Optional: allow user interaction in the custom UI -->
233
+ <!-- Let the custom view receive touches -->
260
234
  <key>UNNotificationExtensionUserInteractionEnabled</key>
261
235
  <true/>
262
236
  </dict>
@@ -267,498 +241,291 @@ The extension's `Info.plist` must declare which notification categories it handl
267
241
  </dict>
268
242
  ```
269
243
 
270
- ### View Controller Implementation
244
+ ### The view controller
271
245
 
272
246
  ```swift
273
247
  import UIKit
274
248
  import UserNotifications
275
249
  import UserNotificationsUI
276
250
 
277
- class NotificationViewController: UIViewController, UNNotificationContentExtension {
278
- @IBOutlet weak var titleLabel: UILabel!
279
- @IBOutlet weak var bodyLabel: UILabel!
280
- @IBOutlet weak var imageView: UIImageView!
251
+ final class NotificationViewController: UIViewController, @preconcurrency UNNotificationContentExtension {
252
+ @IBOutlet private var statusLabel: UILabel!
253
+ @IBOutlet private var photoView: UIImageView!
281
254
 
282
255
  func didReceive(_ notification: UNNotification) {
283
256
  let content = notification.request.content
284
- titleLabel.text = content.title
285
- bodyLabel.text = content.body
286
-
287
- // Display the first attachment
288
- if let attachment = content.attachments.first,
289
- attachment.url.startAccessingSecurityScopedResource() {
290
- defer { attachment.url.stopAccessingSecurityScopedResource() }
291
- if let data = try? Data(contentsOf: attachment.url) {
292
- imageView.image = UIImage(data: data)
293
- }
257
+ statusLabel.text = content.body
258
+
259
+ if let file = content.attachments.first?.url, file.startAccessingSecurityScopedResource() {
260
+ defer { file.stopAccessingSecurityScopedResource() }
261
+ photoView.image = UIImage(contentsOfFile: file.path)
294
262
  }
295
263
  }
296
264
 
297
- /// Called when the user taps a notification action while the content
298
- /// extension is visible.
299
- func didReceive(
300
- _ response: UNNotificationResponse,
301
- completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void
302
- ) {
265
+ func didReceive(_ response: UNNotificationResponse,
266
+ completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {
303
267
  switch response.actionIdentifier {
304
- case "LIKE_ACTION":
305
- // Update UI to show "liked" state
306
- animateLikeConfirmation()
307
- // Dismiss after a short delay
268
+ case "CONFIRM_DELIVERY":
269
+ statusLabel.text = "Thanks, noted."
308
270
  Task {
309
- try? await Task.sleep(for: .seconds(0.5))
271
+ try? await Task.sleep(for: .milliseconds(900))
310
272
  completion(.dismiss)
311
273
  }
312
-
313
- case "REPLY_ACTION":
314
- if let textResponse = response as? UNTextInputNotificationResponse {
315
- // Handle reply inline without opening the app
316
- showReplySentConfirmation(text: textResponse.userText)
317
- Task {
318
- try? await Task.sleep(for: .seconds(1.0))
319
- completion(.dismiss)
320
- }
321
- }
322
-
323
274
  default:
324
- // Forward to the app's notification delegate
325
275
  completion(.dismissAndForwardAction)
326
276
  }
327
277
  }
328
278
  }
329
279
  ```
330
280
 
331
- ### Response Options
281
+ The conformance is marked `@preconcurrency` because the protocol has no
282
+ concurrency annotations while the view controller is main-actor isolated; the
283
+ system drives a content extension on the main thread.
332
284
 
333
- | Option | Behavior |
334
- |--------|----------|
335
- | `.doNotDismiss` | Keep the content extension visible. Use for multi-step interactions. |
336
- | `.dismiss` | Dismiss the notification. The action is handled entirely in the extension. |
337
- | `.dismissAndForwardAction` | Dismiss and forward the action to `UNUserNotificationCenterDelegate.didReceive`. Use when the app needs to handle the action. |
285
+ Attachment files are security scoped inside the extension: wrap access in
286
+ `startAccessingSecurityScopedResource()` and
287
+ `stopAccessingSecurityScopedResource()`.
338
288
 
339
- ### Media Playback in Notifications
289
+ ### Response options
340
290
 
341
- The content extension can play audio or video. Implement `mediaPlayPauseButtonType` and `mediaPlayPauseButtonFrame` for a system-provided play/pause button.
291
+ | Option | Result |
292
+ |--------|--------|
293
+ | `.doNotDismiss` | Stays on screen; for multi-step interactions |
294
+ | `.dismiss` | Closes; the extension handled the action |
295
+ | `.dismissAndForwardAction` | Closes and passes the action to the app's `didReceive` |
342
296
 
343
- ```swift
344
- class MediaNotificationViewController: UIViewController, UNNotificationContentExtension {
345
- var player: AVPlayer?
297
+ ### Media playback
346
298
 
347
- override var mediaPlayPauseButtonType: UNNotificationContentExtensionMediaPlayPauseButtonType {
348
- return .overlay // .none, .default, .overlay
349
- }
299
+ For audio or video, ask the system for its play/pause button and respond to it.
350
300
 
351
- override var mediaPlayPauseButtonFrame: CGRect {
352
- return CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
353
- }
301
+ ```swift
302
+ import AVFoundation
354
303
 
355
- func mediaPlay() {
356
- player?.play()
357
- }
304
+ final class ClipViewController: UIViewController, @preconcurrency UNNotificationContentExtension {
305
+ private var player: AVPlayer?
358
306
 
359
- func mediaPause() {
360
- player?.pause()
361
- }
307
+ var mediaPlayPauseButtonType: UNNotificationContentExtensionMediaPlayPauseButtonType { .overlay }
308
+ var mediaPlayPauseButtonFrame: CGRect { CGRect(x: 16, y: 16, width: 44, height: 44) }
362
309
 
363
310
  func didReceive(_ notification: UNNotification) {
364
- guard let attachment = notification.request.content.attachments.first,
365
- attachment.url.startAccessingSecurityScopedResource() else { return }
366
-
367
- let playerItem = AVPlayerItem(url: attachment.url)
368
- player = AVPlayer(playerItem: playerItem)
369
-
370
- let playerLayer = AVPlayerLayer(player: player)
371
- playerLayer.frame = view.bounds
372
- view.layer.addSublayer(playerLayer)
311
+ guard let url = notification.request.content.attachments.first?.url else { return }
312
+ let player = AVPlayer(url: url)
313
+ let layer = AVPlayerLayer(player: player)
314
+ layer.frame = view.bounds
315
+ view.layer.addSublayer(layer)
316
+ self.player = player
373
317
  }
318
+
319
+ func mediaPlay() { player?.play() }
320
+ func mediaPause() { player?.pause() }
374
321
  }
375
322
  ```
376
323
 
377
- ### Interactive Custom UI
324
+ `mediaPlayPauseButtonType` can be `.none`, `.default` or `.overlay`.
378
325
 
379
- With `UNNotificationExtensionUserInteractionEnabled` set to `true`, the content extension supports gesture recognizers, buttons, and other interactive elements.
380
-
381
- ```swift
382
- class InteractiveNotificationViewController: UIViewController, UNNotificationContentExtension {
383
- private var ratingStars: [UIButton] = []
384
- private var selectedRating = 0
326
+ ### Interactive views
385
327
 
386
- override func viewDidLoad() {
387
- super.viewDidLoad()
388
- setupRatingUI()
389
- }
390
-
391
- func didReceive(_ notification: UNNotification) {
392
- // Configure with notification content
393
- }
328
+ With `UNNotificationExtensionUserInteractionEnabled` set, buttons and gesture
329
+ recognizers in the custom view work like anywhere else in UIKit:
394
330
 
395
- private func setupRatingUI() {
396
- let stack = UIStackView()
397
- stack.axis = .horizontal
398
- stack.spacing = 8
399
- stack.translatesAutoresizingMaskIntoConstraints = false
400
- view.addSubview(stack)
401
-
402
- for i in 1...5 {
403
- let button = UIButton(type: .system)
404
- button.setImage(UIImage(systemName: "star"), for: .normal)
405
- button.tag = i
406
- button.addTarget(self, action: #selector(starTapped(_:)), for: .touchUpInside)
407
- stack.addArrangedSubview(button)
408
- ratingStars.append(button)
409
- }
410
-
411
- NSLayoutConstraint.activate([
412
- stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
413
- stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
414
- ])
415
- }
416
-
417
- @objc private func starTapped(_ sender: UIButton) {
418
- selectedRating = sender.tag
419
- for (index, button) in ratingStars.enumerated() {
420
- let imageName = index < selectedRating ? "star.fill" : "star"
421
- button.setImage(UIImage(systemName: imageName), for: .normal)
422
- }
423
- // Send rating to server
424
- Task {
425
- await submitRating(selectedRating)
426
- }
331
+ ```swift
332
+ private func makeReactionBar(for orderID: String) -> UIStackView {
333
+ let reactions = ["Great", "Okay", "Late"].map { title in
334
+ UIButton(configuration: .tinted(), primaryAction: UIAction(title: title) { _ in
335
+ Task { await FeedbackAPI.submit(orderID: orderID, reaction: title) }
336
+ })
427
337
  }
338
+ let bar = UIStackView(arrangedSubviews: reactions)
339
+ bar.axis = .horizontal
340
+ bar.distribution = .fillEqually
341
+ bar.spacing = 8
342
+ return bar
428
343
  }
429
344
  ```
430
345
 
431
346
  ## Communication Notifications
432
347
 
433
- Communication notifications display the sender's avatar and name prominently. They use SiriKit intents (`INSendMessageIntent` or `INStartCallIntent`) to provide participant information and can have different Focus and summary behavior, so use them only for real person-to-person communication.
348
+ These show the sender's name and picture and are built on the SiriKit intents
349
+ `INSendMessageIntent` and `INStartCallIntent`. They change how Focus and the
350
+ notification summary treat the app, so use them only for messages or calls
351
+ between real people.
434
352
 
435
- ### Setup
436
-
437
- 1. Enable the Communication Notifications capability on the app target.
438
- 2. Add supported intent class names, such as `INSendMessageIntent`, to `NSUserActivityTypes` in `Info.plist`.
439
- 3. Add the `Intents` framework to the Notification Service Extension target.
440
- 4. Configure an `INSendMessageIntent`, create an `INInteraction`, set `direction = .incoming`, donate the interaction, then call `content.updating(from:)` before passing the updated content to the content handler.
353
+ Setup:
354
+ 1. Add the Communication Notifications capability to the app target.
355
+ 2. List the intent classes (for example `INSendMessageIntent`) under
356
+ `NSUserActivityTypes` in the app's Info.plist.
357
+ 3. Link the `Intents` framework into the service extension.
358
+ 4. In the extension: build the intent, wrap it in
359
+ `INInteraction(intent:response:)`, set `direction = .incoming`, call
360
+ `donate()`, then pass `content.updating(from:)` to the handler.
441
361
 
442
362
  ```swift
443
363
  import Intents
444
- import UserNotifications
445
-
446
- extension NotificationService {
447
- func configureCommunicationNotification(
448
- content: UNMutableNotificationContent,
449
- senderName: String,
450
- senderImageURL: String?,
451
- conversationId: String
452
- ) async -> UNNotificationContent? {
453
- // Create the sender identity
454
- let handle = INPersonHandle(value: conversationId, type: .unknown)
455
- var avatar: INImage? = nil
456
-
457
- // Download sender avatar
458
- if let urlString = senderImageURL,
459
- let url = URL(string: urlString) {
460
- if let (data, _) = try? await URLSession.shared.data(from: url) {
461
- avatar = INImage(imageData: data)
462
- }
463
- }
464
-
465
- let nameComponents = PersonNameComponentsFormatter()
466
- .personNameComponents(from: senderName)
467
364
 
365
+ enum SenderStyling {
366
+ static func apply(to content: UNNotificationContent,
367
+ senderID: String,
368
+ senderName: String,
369
+ avatar: Data?,
370
+ conversationID: String) async -> UNNotificationContent {
468
371
  let sender = INPerson(
469
- personHandle: handle,
470
- nameComponents: nameComponents,
372
+ personHandle: INPersonHandle(value: senderID, type: .unknown),
373
+ nameComponents: PersonNameComponentsFormatter().personNameComponents(from: senderName),
471
374
  displayName: senderName,
472
- image: avatar,
375
+ image: avatar.map { INImage(imageData: $0) },
473
376
  contactIdentifier: nil,
474
- customIdentifier: conversationId
377
+ customIdentifier: senderID
475
378
  )
476
-
477
- // Create the messaging intent
478
379
  let intent = INSendMessageIntent(
479
380
  recipients: nil,
480
381
  outgoingMessageType: .outgoingMessageText,
481
382
  content: content.body,
482
383
  speakableGroupName: nil,
483
- conversationIdentifier: conversationId,
384
+ conversationIdentifier: conversationID,
484
385
  serviceName: nil,
485
386
  sender: sender,
486
387
  attachments: nil
487
388
  )
488
-
489
- // Donate the interaction so Siri learns about this contact
490
- let interaction = INInteraction(intent: intent, response: nil)
491
- interaction.direction = .incoming
492
- try? await interaction.donate()
389
+ let donation = INInteraction(intent: intent, response: nil)
390
+ donation.direction = .incoming
391
+ try? await donation.donate()
493
392
 
494
393
  do {
495
- return try content.updating(from: intent)
394
+ let styled = try content.updating(from: intent)
395
+ return styled
496
396
  } catch {
497
- print("Failed to update content with intent: \(error)")
498
- return nil
397
+ return content // fall back to the plain notification
499
398
  }
500
399
  }
501
400
  }
502
401
  ```
503
402
 
504
- ### Handling in the Service Extension
403
+ ### In the service extension
505
404
 
506
- The complete flow integrates communication notifications into the standard service extension:
405
+ Attach media first. Then, if the payload names a sender, run the styling
406
+ above and deliver its result; if not, deliver the plain content. The
407
+ [complete example](#a-complete-service-extension) shows the whole flow.
507
408
 
508
- ```swift
509
- override func didReceive(
510
- _ request: UNNotificationRequest,
511
- withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
512
- ) {
513
- guard let content = request.content.mutableCopy() as? UNMutableNotificationContent else {
514
- contentHandler(request.content)
515
- return
516
- }
409
+ ## Troubleshooting Extensions
517
410
 
518
- Task {
519
- // Download and attach image
520
- if let imageUrl = content.userInfo["imageUrl"] as? String {
521
- await attachImage(from: imageUrl, to: content)
522
- }
411
+ **Service extension never runs**
412
+ - The payload includes an alert and sets `mutable-content: 1`.
413
+ - The extension is embedded in the app and has a valid bundle ID and profile.
414
+ - Both targets carry the shared App Group and Keychain entitlements they use.
523
415
 
524
- // Configure as communication notification if sender info is present
525
- if let senderName = content.userInfo["senderName"] as? String,
526
- let conversationId = content.userInfo["conversationId"] as? String {
527
- let senderImage = content.userInfo["senderImageUrl"] as? String
528
-
529
- let handle = INPersonHandle(value: conversationId, type: .unknown)
530
- var avatar: INImage? = nil
531
- if let urlString = senderImage,
532
- let url = URL(string: urlString),
533
- let (data, _) = try? await URLSession.shared.data(from: url) {
534
- avatar = INImage(imageData: data)
535
- }
416
+ **Content extension never appears**
417
+ - `UNNotificationExtensionCategory` matches the notification's
418
+ `categoryIdentifier` exactly.
419
+ - The storyboard or view is actually set up, and the size ratio is sensible.
536
420
 
537
- let nameComponents = PersonNameComponentsFormatter()
538
- .personNameComponents(from: senderName)
421
+ **Memory**
422
+ - The system kills extensions that use too much memory. Keep dependencies
423
+ small and use `URLSession` directly rather than a large networking library.
424
+ A service extension has no UI, so a UI framework buys nothing there; a
425
+ content extension may host SwiftUI through `UIHostingController`, but keep
426
+ it light.
539
427
 
540
- let sender = INPerson(
541
- personHandle: handle,
542
- nameComponents: nameComponents,
543
- displayName: senderName,
544
- image: avatar,
545
- contactIdentifier: nil,
546
- customIdentifier: conversationId
547
- )
428
+ **Fallbacks**
429
+ - The handler runs exactly once. If a download, decryption, donation or
430
+ `updating(from:)` fails, deliver the original or the best attempt so far.
431
+ - In `serviceExtensionTimeWillExpire()`, stop waiting and deliver the best
432
+ content right away.
548
433
 
549
- let intent = INSendMessageIntent(
550
- recipients: nil,
551
- outgoingMessageType: .outgoingMessageText,
552
- content: content.body,
553
- speakableGroupName: nil,
554
- conversationIdentifier: conversationId,
555
- serviceName: nil,
556
- sender: sender,
557
- attachments: nil
558
- )
434
+ **Network**
435
+ - Extensions may use the network; a `URLSession` with the `.default`
436
+ configuration is fine. Stay well inside the roughly 30-second budget and
437
+ fall back to the payload text on failure.
559
438
 
560
- let interaction = INInteraction(intent: intent, response: nil)
561
- interaction.direction = .incoming
562
- try? await interaction.donate()
439
+ **Debugging**
440
+ - Run the extension's scheme, or attach from the Debug menu (Attach to
441
+ Process by PID or Name) using the extension's name.
442
+ - Log with `os_log` or `Logger` and filter Console.app by the extension's
443
+ bundle ID.
444
+ - Set breakpoints, then send a notification to trigger the extension.
563
445
 
564
- if let updatedContent = try? content.updating(from: intent) {
565
- contentHandler(updatedContent)
566
- return
567
- }
568
- }
446
+ ## A Complete Service Extension
569
447
 
570
- contentHandler(content)
571
- }
572
- }
573
- ```
574
-
575
- ## Extension Gotchas
576
-
577
- **Service extension not running:**
578
- - Verify `mutable-content: 1` is set in the APNs payload.
579
- - The notification must have an alert (title or body). Silent pushes do not trigger the service extension.
580
- - Confirm the service extension target is embedded in the containing app and its bundle identifier/provisioning profile are valid.
581
- - Confirm both targets include the same App Group or Keychain Sharing entitlements if they share data.
582
-
583
- **Content extension not showing:**
584
- - Verify the `UNNotificationExtensionCategory` in Info.plist matches the `categoryIdentifier` in the notification.
585
- - Check that the storyboard or programmatic UI is properly configured.
586
- - Ensure `UNNotificationExtensionInitialContentSizeRatio` is set to a reasonable value.
587
-
588
- **Memory pressure:**
589
- - Extensions are memory-constrained and can be terminated under pressure.
590
- - Avoid loading large frameworks (no SwiftUI, no heavy networking libraries).
591
- - Use `URLSession` directly for network requests in extensions.
592
-
593
- **Fallback handling:**
594
- - Always call the content handler exactly once on every path.
595
- - On download, decryption, donation, or `content.updating(from:)` failure, return the original or best-attempt content rather than dropping the notification.
596
- - In `serviceExtensionTimeWillExpire()`, stop waiting for in-flight work and call the content handler immediately with the best content available.
597
-
598
- **Network access in extensions:**
599
- - Extensions can make network requests. Use `URLSession` with the `.default` configuration.
600
- - Keep requests fast -- the service extension has approximately 30 seconds total.
601
- - Handle network failures gracefully; fall back to showing what the original payload contains.
602
-
603
- **Debugging extensions:**
604
- - In Xcode, select the extension scheme and attach to the extension process.
605
- - Use `Debug > Attach to Process by PID or Name` with the extension's process name.
606
- - Use `os_log` or `print` statements and view them in Console.app filtered by the extension's bundle identifier.
607
- - Set breakpoints in the extension target and trigger a notification to hit them.
608
-
609
- ## Complete Service Extension Example
610
-
611
- A production-ready service extension that handles image download, body decryption, and communication notifications:
448
+ The steps run in order: decrypt the body with a key from the shared Keychain,
449
+ attach media, style as a communication notification, then deliver once. The
450
+ timeout path delivers whatever is ready.
612
451
 
613
452
  ```swift
614
- import UserNotifications
615
453
  import Intents
454
+ import UserNotifications
616
455
 
617
- class NotificationService: UNNotificationServiceExtension {
618
- private var contentHandler: ((UNNotificationContent) -> Void)?
619
- private var bestAttemptContent: UNMutableNotificationContent?
456
+ final class NotificationService: UNNotificationServiceExtension, @unchecked Sendable {
457
+ private let lock = NSLock()
458
+ private var handler: ((UNNotificationContent) -> Void)?
459
+ private var bestAttempt: UNNotificationContent?
620
460
 
621
461
  override func didReceive(
622
462
  _ request: UNNotificationRequest,
623
463
  withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
624
464
  ) {
625
- self.contentHandler = contentHandler
626
- bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
627
-
628
- guard let content = bestAttemptContent else {
629
- contentHandler(request.content)
630
- return
465
+ lock.withLock {
466
+ handler = contentHandler
467
+ bestAttempt = request.content
631
468
  }
469
+ let payload = IncomingPayload(request.content.userInfo)
632
470
 
633
471
  Task {
634
- // Step 1: Decrypt body if needed
635
- if let encrypted = content.userInfo["encryptedBody"] as? String,
636
- let decrypted = decryptBody(encrypted) {
637
- content.body = decrypted
472
+ guard let draft = self.snapshot()?.mutableCopy() as? UNMutableNotificationContent else {
473
+ self.finish(with: nil)
474
+ return
638
475
  }
639
476
 
640
- // Step 2: Download and attach image
641
- if let imageUrl = content.userInfo["imageUrl"] as? String {
642
- await attachMedia(from: imageUrl, to: content)
643
- }
477
+ // 1. Decrypt, falling back to neutral text.
478
+ draft.body = payload.decryptedBody() ?? "New message"
479
+ self.remember(draft)
644
480
 
645
- // Step 3: Configure communication notification
646
- if let senderName = content.userInfo["senderName"] as? String,
647
- let convId = content.userInfo["conversationId"] as? String {
648
- if let updated = await configureAsCommunication(
649
- content: content,
650
- senderName: senderName,
651
- senderImageURL: content.userInfo["senderImage"] as? String,
652
- conversationId: convId
653
- ) {
654
- finish(with: updated)
655
- return
656
- }
481
+ // 2. Media, skipped silently on failure.
482
+ if let url = payload.mediaURL, let media = await MediaFetcher.attachment(from: url) {
483
+ draft.attachments = [media]
484
+ self.remember(draft)
657
485
  }
658
486
 
659
- finish(with: content)
487
+ // 3. Sender styling, only when the payload names a sender.
488
+ guard let sender = payload.sender else {
489
+ self.finish(with: draft)
490
+ return
491
+ }
492
+ let styled = await SenderStyling.apply(
493
+ to: draft,
494
+ senderID: sender.id,
495
+ senderName: sender.name,
496
+ avatar: await payload.avatarData(),
497
+ conversationID: sender.conversationID
498
+ )
499
+ self.finish(with: styled)
660
500
  }
661
501
  }
662
502
 
663
503
  override func serviceExtensionTimeWillExpire() {
664
- if let content = bestAttemptContent {
665
- finish(with: content)
666
- }
504
+ finish(with: nil) // best attempt so far
667
505
  }
668
506
 
669
- // MARK: - Private
670
-
671
- private func finish(with content: UNNotificationContent) {
672
- guard !didComplete, let handler = contentHandler else { return }
673
- didComplete = true
674
- handler(content)
675
- }
676
-
677
- private func decryptBody(_ base64: String) -> String? {
678
- guard let data = Data(base64Encoded: base64) else { return nil }
679
- guard let keyData = SharedKeychain.loadData(
680
- account: "notificationEncryptionKey"
681
- ) else { return nil }
682
- // Decrypt using CryptoKit or similar
683
- return try? Decryptor.decrypt(data, key: keyData)
507
+ private func snapshot() -> UNNotificationContent? {
508
+ lock.withLock { bestAttempt }
684
509
  }
685
510
 
686
- private func attachMedia(
687
- from urlString: String,
688
- to content: UNMutableNotificationContent
689
- ) async {
690
- guard let url = URL(string: urlString) else { return }
691
- do {
692
- let (data, response) = try await URLSession.shared.data(from: url)
693
- let ext = fileExtension(for: response.mimeType)
694
- let fileURL = FileManager.default.temporaryDirectory
695
- .appendingPathComponent(UUID().uuidString + ext)
696
- try data.write(to: fileURL)
697
- let attachment = try UNNotificationAttachment(
698
- identifier: UUID().uuidString,
699
- url: fileURL,
700
- options: nil
701
- )
702
- content.attachments = [attachment]
703
- } catch {
704
- // Image download failed -- notification displays without media
705
- }
706
- }
707
-
708
- private func configureAsCommunication(
709
- content: UNMutableNotificationContent,
710
- senderName: String,
711
- senderImageURL: String?,
712
- conversationId: String
713
- ) async -> UNNotificationContent? {
714
- let handle = INPersonHandle(value: conversationId, type: .unknown)
715
- var avatar: INImage? = nil
716
-
717
- if let urlStr = senderImageURL,
718
- let url = URL(string: urlStr),
719
- let (data, _) = try? await URLSession.shared.data(from: url) {
720
- avatar = INImage(imageData: data)
721
- }
722
-
723
- let nameComponents = PersonNameComponentsFormatter()
724
- .personNameComponents(from: senderName)
725
-
726
- let sender = INPerson(
727
- personHandle: handle,
728
- nameComponents: nameComponents,
729
- displayName: senderName,
730
- image: avatar,
731
- contactIdentifier: nil,
732
- customIdentifier: conversationId
733
- )
734
-
735
- let intent = INSendMessageIntent(
736
- recipients: nil,
737
- outgoingMessageType: .outgoingMessageText,
738
- content: content.body,
739
- speakableGroupName: nil,
740
- conversationIdentifier: conversationId,
741
- serviceName: nil,
742
- sender: sender,
743
- attachments: nil
744
- )
745
-
746
- let interaction = INInteraction(intent: intent, response: nil)
747
- interaction.direction = .incoming
748
- try? await interaction.donate()
749
-
750
- return try? content.updating(from: intent)
511
+ private func remember(_ content: UNNotificationContent) {
512
+ lock.withLock { if handler != nil { bestAttempt = content.copy() as? UNNotificationContent } }
751
513
  }
752
514
 
753
- private func fileExtension(for mimeType: String?) -> String {
754
- switch mimeType {
755
- case "image/jpeg": return ".jpg"
756
- case "image/png": return ".png"
757
- case "image/gif": return ".gif"
758
- case "video/mp4": return ".mp4"
759
- case "audio/mpeg": return ".mp3"
760
- default: return ".jpg"
515
+ /// Single delivery point. `nil` means "use the best attempt".
516
+ private func finish(with ready: UNNotificationContent?) {
517
+ let delivery: (((UNNotificationContent) -> Void), UNNotificationContent)? = lock.withLock {
518
+ guard let handler, let fallback = bestAttempt else { return nil }
519
+ self.handler = nil
520
+ return (handler, ready ?? fallback)
761
521
  }
522
+ guard let (deliverTo, result) = delivery else { return }
523
+ deliverTo(result)
762
524
  }
763
525
  }
764
526
  ```
527
+
528
+ `IncomingPayload` is the app's own parser over `userInfo`: it reads the sealed
529
+ body and decrypts it with the shared-Keychain key, and exposes `mediaURL`, the
530
+ sender fields and an avatar download. Keep it free of UI and large
531
+ dependencies.