@mmerterden/multi-agent-pipeline 20.6.0 → 20.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +328 -329
  5. package/package.json +2 -2
  6. package/pipeline/multi-agent-refs/features/design-conformance.md +62 -64
  7. package/pipeline/scripts/_notices.mjs +12 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  9. package/pipeline/skills/.skill-manifest.json +106 -106
  10. package/pipeline/skills/shared/README.md +71 -71
  11. package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +1 -0
  12. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  13. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  14. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  15. package/pipeline/skills/shared/external/android-architecture/SKILL.md +2 -0
  16. package/pipeline/skills/shared/external/android-performance/SKILL.md +2 -0
  17. package/pipeline/skills/shared/external/android-security/SKILL.md +2 -0
  18. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  19. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  20. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  21. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  22. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  23. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  24. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  25. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  26. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  27. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  28. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  29. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  30. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  31. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  32. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  33. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  34. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  35. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  37. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  38. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  39. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  40. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  41. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  42. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  43. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  44. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  45. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  46. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  47. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  48. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  49. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  50. package/pipeline/skills/shared/external/backlog/BACKLOG.md +1 -1
  51. package/pipeline/skills/shared/external/backlog/SKILL.md +56 -33
  52. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  53. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  54. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  55. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +1 -0
  56. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  57. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  58. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  59. package/pipeline/skills/shared/external/compose-components/SKILL.md +2 -0
  60. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +3 -2
  61. package/pipeline/skills/shared/external/compose-testing/SKILL.md +2 -0
  62. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  63. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  64. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  65. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  66. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  67. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  68. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  69. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  70. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  71. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  72. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  73. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  74. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  75. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  76. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  77. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  78. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  79. package/pipeline/skills/shared/external/council/SKILL.md +1 -0
  80. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  81. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  82. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  83. package/pipeline/skills/shared/external/css-modern/SKILL.md +1 -0
  84. package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -0
  85. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  86. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  87. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  88. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  89. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  90. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  91. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  92. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  93. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  94. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  95. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  96. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  97. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  98. package/pipeline/skills/shared/external/evidence-github/SKILL.md +2 -0
  99. package/pipeline/skills/shared/external/evidence-registry/SKILL.md +2 -0
  100. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +2 -0
  101. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  102. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  103. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  104. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  105. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  106. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  107. package/pipeline/skills/shared/external/html-semantic/SKILL.md +1 -0
  108. package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -0
  109. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  110. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  111. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  112. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  113. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  114. package/pipeline/skills/shared/external/ios-coding-standard/SKILL.md +1 -0
  115. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  116. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  117. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  118. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  119. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +1 -0
  120. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  121. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  122. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  123. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  124. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  125. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  126. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  127. package/pipeline/skills/shared/external/ios-security/SKILL.md +2 -0
  128. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  129. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  130. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  131. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  132. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  133. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  134. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +91 -283
  135. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +119 -151
  136. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +60 -90
  137. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +119 -156
  138. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +726 -787
  139. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +253 -288
  140. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +243 -304
  141. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +88 -104
  142. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +181 -235
  143. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +198 -263
  144. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +461 -466
  145. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +145 -151
  146. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +123 -141
  147. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +146 -157
  148. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +22 -19
  149. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +156 -140
  150. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  151. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  152. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  153. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  154. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  155. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  156. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  157. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  158. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  159. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  160. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  161. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  162. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  163. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +1 -0
  164. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  165. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  166. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  167. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  168. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  169. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  170. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  171. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  172. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  173. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  174. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  175. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  176. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  177. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  178. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  179. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  180. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  181. package/pipeline/skills/shared/external/play-store-review/SKILL.md +2 -0
  182. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  183. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  184. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  185. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  186. package/pipeline/skills/shared/external/python-patterns/SKILL.md +1 -0
  187. package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -0
  188. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  189. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  190. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  191. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +1 -0
  192. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +2 -0
  193. package/pipeline/skills/shared/external/room-database/SKILL.md +2 -0
  194. package/pipeline/skills/shared/external/search-first/SKILL.md +1 -0
  195. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  196. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  197. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  198. package/pipeline/skills/shared/external/signal-community/SKILL.md +2 -0
  199. package/pipeline/skills/shared/external/skill-creator/SKILL.md +80 -41
  200. package/pipeline/skills/shared/external/skill-creator/audit.md +63 -59
  201. package/pipeline/skills/shared/external/skill-creator/checklist.md +28 -20
  202. package/pipeline/skills/shared/external/skill-creator/examples.md +40 -40
  203. package/pipeline/skills/shared/external/skill-creator/label-check.md +48 -36
  204. package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +91 -100
  205. package/pipeline/skills/shared/external/skill-creator/template.md +51 -39
  206. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  207. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  208. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  209. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  210. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  211. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  212. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  213. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  214. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  215. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  216. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  217. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  218. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  219. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  220. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  221. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  222. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  223. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  224. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  225. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  226. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  227. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  228. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  229. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  230. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  231. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  232. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  233. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  234. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  235. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  236. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  237. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  238. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  239. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  240. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  241. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  242. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  243. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  244. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  245. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  246. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  247. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  248. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  249. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  250. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  251. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  252. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  253. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  254. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  255. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  256. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  257. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  258. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  259. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  260. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  261. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  262. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  263. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  264. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  265. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  266. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  267. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  268. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  269. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  270. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  271. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  272. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  273. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  274. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  275. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  276. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  277. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  278. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  279. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  280. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  281. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  282. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  283. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  284. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  285. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  286. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  287. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  288. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  289. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  290. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  291. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  292. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  293. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  294. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  295. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  296. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  297. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  298. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  299. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  300. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  301. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  302. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  303. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  304. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  305. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  306. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  307. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  308. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  309. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  310. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +1 -0
  311. package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -0
  312. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  313. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  314. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +1 -0
  315. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  316. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  317. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  318. package/pipeline/skills/shared/external/vue-composition/SKILL.md +1 -0
  319. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  320. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  321. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +1 -0
  322. package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -0
  323. package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -0
  324. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  325. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  326. 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.