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