@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1

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 (284) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +285 -285
  5. package/package.json +3 -3
  6. package/pipeline/lib/redact.mjs +3 -2
  7. package/pipeline/scripts/_notices.mjs +1 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  9. package/pipeline/scripts/pre-commit-check.sh +4 -0
  10. package/pipeline/skills/.skill-manifest.json +69 -69
  11. package/pipeline/skills/shared/README.md +70 -70
  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/app-clips/SKILL.md +260 -160
  16. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  17. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  18. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  19. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  20. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  21. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  22. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  23. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
  24. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  25. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
  26. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
  27. package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
  28. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  29. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  30. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  31. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
  32. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  34. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  35. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  37. package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
  38. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  39. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
  40. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  41. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  42. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  43. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  44. package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
  45. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  46. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
  47. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  48. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  49. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  50. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  51. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  52. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  53. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  54. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  55. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  56. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
  57. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  58. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  59. package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
  60. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  61. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  62. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  63. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  64. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  65. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  66. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  67. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  68. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  69. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  70. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  71. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  72. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  73. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  74. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  75. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  76. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  77. package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
  78. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  79. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  80. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  81. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  82. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  83. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  84. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  85. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  86. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  87. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  88. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  89. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  90. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  91. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  92. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  93. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  94. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  95. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  96. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  97. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  98. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  99. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  100. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  101. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  102. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  103. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  104. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  105. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  106. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  107. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  108. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  109. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  111. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  112. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  113. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  114. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  115. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  117. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  118. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  129. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  130. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
  131. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  132. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  133. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  134. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  135. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  136. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  137. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  138. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  139. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  140. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  141. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  142. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  143. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  144. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  145. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  146. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  147. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  148. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  149. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  150. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  151. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  152. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  153. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  154. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  155. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  156. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  157. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  158. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  159. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  160. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  161. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  162. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  163. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  164. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  165. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  166. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  167. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  168. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  169. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  170. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  171. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  172. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  173. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  174. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  175. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  176. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  177. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
  178. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  179. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  180. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  181. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  182. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  183. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  184. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  185. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  186. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  187. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  188. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  189. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  191. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
  193. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  194. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  195. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  196. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  197. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  198. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  199. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  200. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  201. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  202. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  203. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  204. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  205. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  206. package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
  207. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  208. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  209. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
  210. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  211. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  212. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  213. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  214. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  215. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  216. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
  217. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  218. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  219. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  220. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
  221. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  222. package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
  223. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  224. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
  225. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  226. package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
  227. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  228. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  229. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  230. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  231. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  232. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  233. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  234. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  235. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  236. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  237. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  238. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  239. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  240. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  241. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  242. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  243. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  244. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  245. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  246. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  247. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  248. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  249. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  250. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  251. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
  252. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  253. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
  254. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  255. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
  256. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  257. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  258. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  259. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  260. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  261. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  262. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  263. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  264. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  265. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  266. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  267. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  268. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  269. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  270. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  271. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  272. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  273. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  274. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  275. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  276. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  277. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  278. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  279. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  280. package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
  281. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  282. package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
  283. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
  284. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,46 +1,43 @@
1
1
  # Design Polish
2
2
 
3
- ## Contents
4
- - [HIG Alignment](#hig-alignment)
5
- - [Theming and Dynamic Type](#theming-and-dynamic-type)
6
- - [Haptics](#haptics)
7
- - [Matched Transitions](#matched-transitions)
8
- - [Loading and Placeholders](#loading-and-placeholders)
9
- - [Focus Handling](#focus-handling)
10
-
11
- ## HIG Alignment
12
-
13
- iOS Human Interface Guidelines patterns for layout, typography, color, accessibility, and feedback in SwiftUI.
14
-
15
- ### Contents
16
-
17
- - [Layout and Spacing](#layout-and-spacing)
18
- - [Typography](#typography)
19
- - [Color System](#color-system)
20
- - [Navigation Patterns](#navigation-patterns)
21
- - [Feedback](#feedback)
22
- - [Accessibility](#accessibility)
23
- - [Error and Empty States](#error-and-empty-states)
24
-
25
- ### Layout and Spacing
26
-
27
- #### Spacing Grid
3
+ Human Interface Guidelines applied in SwiftUI (layout, type, color, navigation
4
+ chrome, feedback, accessibility, empty states), plus theming, haptics, matched
5
+ transitions, loading placeholders and form focus.
28
6
 
29
- Omit `spacing:` on stacks to get SwiftUI's adaptive default. Only specify an explicit value when you need a deliberate departure from the default - and when you do, stick to the 4pt grid below.
7
+ ## Contents
30
8
 
31
- This is a common design convention, not an Apple-prescribed system, but it keeps layouts visually coherent. Avoid inventing values between grid stops.
9
+ 1. [HIG: Layout and Spacing](#1-hig-layout-and-spacing)
10
+ 2. [HIG: Typography](#2-hig-typography)
11
+ 3. [HIG: Color](#3-hig-color)
12
+ 4. [HIG: Navigation Chrome](#4-hig-navigation-chrome)
13
+ 5. [HIG: Feedback](#5-hig-feedback)
14
+ 6. [HIG: Accessibility](#6-hig-accessibility)
15
+ 7. [HIG: Error and Empty States](#7-hig-error-and-empty-states)
16
+ 8. [Theming and Dynamic Type](#8-theming-and-dynamic-type)
17
+ 9. [Haptics](#9-haptics)
18
+ 10. [Core Haptics](#10-core-haptics)
19
+ 11. [Matched Transitions](#11-matched-transitions)
20
+ 12. [Loading and Placeholders](#12-loading-and-placeholders)
21
+ 13. [Form Focus](#13-form-focus)
22
+
23
+ ## 1. HIG: Layout and Spacing
24
+
25
+ Start by leaving `spacing:` off stacks; the adaptive default is usually right.
26
+ When you do choose a value on purpose, take it from a 4 point grid. That grid is
27
+ a widespread design convention, not something Apple requires, and values
28
+ between its stops should not be invented.
32
29
 
33
30
  | Points | Token | Typical use |
34
- |--------|-------|-------------|
35
- | 4 | `.xxSmall` | Tight icon-to-label padding, inline badge offsets |
36
- | 8 | `.xSmall` | Related elements within a group, compact stack gaps |
37
- | 12 | `.small` | List row internal padding, label-to-secondary-text |
38
- | 16 | `.medium` | Standard margin, default section gap |
39
- | 20 | `.mediumLarge` | Comfortable breathing room between distinct controls |
40
- | 24 | `.large` | Section separators, card internal padding |
41
- | 32 | `.xLarge` | Major groupings, header-to-content gap |
42
- | 40 | `.xxLarge` | Large section breaks |
43
- | 48 | `.xxxLarge` | Hero/splash spacing, onboarding screens |
31
+ |---|---|---|
32
+ | 4 | `xxSmall` | Icon to label, inline badge offset |
33
+ | 8 | `xSmall` | Related items in one group, tight stack gaps |
34
+ | 12 | `small` | Row inner padding, label to secondary text |
35
+ | 16 | `medium` | Standard margin, default gap between sections |
36
+ | 20 | `mediumLarge` | Room between separate controls |
37
+ | 24 | `large` | Section separators, card inner padding |
38
+ | 32 | `xLarge` | Major groups, header to content |
39
+ | 40 | `xxLarge` | Large section breaks |
40
+ | 48 | `xxxLarge` | Hero, splash and onboarding layouts |
44
41
 
45
42
  ```swift
46
43
  enum Spacing {
@@ -56,477 +53,416 @@ enum Spacing {
56
53
  }
57
54
  ```
58
55
 
59
- #### Standard Margins
56
+ Margins shared across screens can be named insets:
60
57
 
61
58
  ```swift
62
- private let standardMargin: CGFloat = 16
63
- private let compactMargin: CGFloat = 8
64
- private let largeMargin: CGFloat = 24
59
+ private enum Margin {
60
+ static let regular: CGFloat = 16
61
+ static let tight: CGFloat = 8
62
+ static let wide: CGFloat = 24
63
+ }
65
64
 
66
65
  extension EdgeInsets {
67
- static let standard = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
68
- static let listRow = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)
66
+ static let regular = EdgeInsets(top: Margin.regular, leading: Margin.regular,
67
+ bottom: Margin.regular, trailing: Margin.regular)
68
+ static let rowContent = EdgeInsets(top: 12, leading: Margin.regular,
69
+ bottom: 12, trailing: Margin.regular)
69
70
  }
70
71
  ```
71
72
 
72
- #### Safe Area Handling
73
+ Respect the safe area and pin persistent actions with `safeAreaInset`:
73
74
 
74
75
  ```swift
75
76
  ScrollView {
76
- LazyVStack {
77
- ForEach(items) { item in
78
- ItemRow(item: item)
79
- }
80
- }
81
- .padding(.horizontal)
77
+ LazyVStack { ForEach(lines) { OrderLineRow(line: $0) } }
78
+ .padding(.horizontal)
82
79
  }
83
80
  .safeAreaInset(edge: .bottom) {
84
81
  HStack {
85
- Button("Cancel") { }
86
- .buttonStyle(.bordered)
87
- Spacer()
88
- Button("Confirm") { }
89
- .buttonStyle(.borderedProminent)
82
+ Button("Back", action: goBack).buttonStyle(.bordered)
83
+ Button("Place order", action: placeOrder).buttonStyle(.borderedProminent)
90
84
  }
91
85
  .padding()
92
86
  .background(.regularMaterial)
93
87
  }
94
88
  ```
95
89
 
96
- #### Adaptive Layouts
97
-
98
- Use `horizontalSizeClass` to adapt between compact and regular widths:
90
+ Adapt compact and regular widths with the size class:
99
91
 
100
92
  ```swift
101
93
  @Environment(\.horizontalSizeClass) private var sizeClass
102
94
 
103
- private var columns: [GridItem] {
104
- switch sizeClass {
105
- case .compact:
106
- [GridItem(.flexible())]
107
- case .regular:
108
- [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
109
- default:
110
- [GridItem(.flexible())]
111
- }
95
+ private var gridColumns: [GridItem] {
96
+ let count = sizeClass == .regular ? 3 : 1
97
+ return Array(repeating: GridItem(.flexible()), count: count)
112
98
  }
113
99
  ```
114
100
 
115
- ### Typography
101
+ ## 2. HIG: Typography
116
102
 
117
- #### System Font Styles
103
+ Text styles scale with Dynamic Type without extra work. Sizes below are the
104
+ iOS defaults at the Large content size.
118
105
 
119
- Use system font styles for automatic Dynamic Type support:
106
+ | Style | Size and weight | Use |
107
+ |---|---|---|
108
+ | `.largeTitle` | 34 pt regular | Screen title |
109
+ | `.title` | 28 pt regular | Section header |
110
+ | `.title2` | 22 pt regular | Sub-section header |
111
+ | `.title3` | 20 pt regular | Group header |
112
+ | `.headline` | 17 pt semibold | Row title |
113
+ | `.body` | 17 pt regular | Main content |
114
+ | `.callout` | 16 pt regular | Secondary content |
115
+ | `.subheadline` | 15 pt regular | Supporting text |
116
+ | `.footnote` | 13 pt regular | Tertiary information |
117
+ | `.caption` | 12 pt regular | Labels |
118
+ | `.caption2` | 11 pt regular | Small labels |
120
119
 
121
- | Style | Size | Weight | Usage |
122
- |-------|------|--------|-------|
123
- | `.largeTitle` | 34pt | Regular | Screen titles |
124
- | `.title` | 28pt | Regular | Section headers |
125
- | `.title2` | 22pt | Regular | Sub-section headers |
126
- | `.title3` | 20pt | Regular | Group headers |
127
- | `.headline` | 17pt | Semibold | Row titles |
128
- | `.body` | 17pt | Regular | Primary content |
129
- | `.callout` | 16pt | Regular | Secondary content |
130
- | `.subheadline` | 15pt | Regular | Supporting text |
131
- | `.footnote` | 13pt | Regular | Tertiary info |
132
- | `.caption` | 12pt | Regular | Labels |
133
- | `.caption2` | 11pt | Regular | Small labels |
134
-
135
- #### Custom Font with Dynamic Type
120
+ A custom typeface keeps Dynamic Type when it is tied to a text style:
136
121
 
137
122
  ```swift
138
123
  extension Font {
139
- static func customBody(_ name: String) -> Font {
140
- .custom(name, size: 17, relativeTo: .body)
124
+ static func brandBody(_ family: String) -> Font {
125
+ .custom(family, size: 17, relativeTo: .body)
141
126
  }
142
127
  }
143
128
  ```
144
129
 
145
- ### Color System
146
-
147
- #### Semantic Colors
148
-
149
- Use semantic colors for automatic light/dark mode support:
150
-
151
- ```swift
152
- // Labels
153
- Color.primary // Primary text
154
- Color.secondary // Secondary text
155
- Color(uiColor: .tertiaryLabel)
156
-
157
- // Backgrounds
158
- Color(uiColor: .systemBackground)
159
- Color(uiColor: .secondarySystemBackground)
160
- Color(uiColor: .systemGroupedBackground)
161
-
162
- // Fills and Separators
163
- Color(uiColor: .systemFill)
164
- Color(uiColor: .separator)
165
- ```
166
-
167
- #### Tint Colors
130
+ ## 3. HIG: Color
168
131
 
169
- ```swift
170
- // Apply app-wide tint
171
- ContentView()
172
- .tint(.blue)
173
- ```
132
+ Semantic colors switch between light and dark appearance on their own.
174
133
 
175
- Use `.tint(...)` or `.foregroundStyle(.tint)` for interactive elements and `Color.red` for destructive actions.
134
+ | Role | Colors |
135
+ |---|---|
136
+ | Labels | `Color.primary`, `Color.secondary`, `Color(uiColor: .tertiaryLabel)` |
137
+ | Backgrounds | `Color(uiColor: .systemBackground)`, `.secondarySystemBackground`, `.systemGroupedBackground` |
138
+ | Fill and divider | `.systemFill` and `.separator`, both wrapped in `Color(uiColor:)` |
176
139
 
177
- ### Navigation Patterns
140
+ - Set the app tint once on the root view, for example `.tint(.indigo)`.
141
+ - Interactive elements use the tint through `.tint(...)` or
142
+ `.foregroundStyle(.tint)`.
143
+ - Destructive actions use red (`Color.red`, or a `.destructive` button role).
178
144
 
179
- #### Hierarchical (NavigationSplitView)
145
+ ## 4. HIG: Navigation Chrome
180
146
 
181
- Use for iPad/macOS multi-column layouts:
147
+ Multi-column hierarchies on iPad and Mac use `NavigationSplitView`:
182
148
 
183
149
  ```swift
184
150
  NavigationSplitView {
185
- List(items, selection: $selectedItem) { item in
186
- NavigationLink(value: item) { ItemRow(item: item) }
151
+ List(folders, selection: $selectedFolder) { folder in
152
+ NavigationLink(folder.name, value: folder)
187
153
  }
188
- .navigationTitle("Items")
154
+ .navigationTitle("Folders")
189
155
  } detail: {
190
- if let item = selectedItem {
191
- ItemDetailView(item: item)
156
+ if let selectedFolder {
157
+ FolderDetail(folder: selectedFolder)
192
158
  } else {
193
- ContentUnavailableView("Select an Item", systemImage: "sidebar.leading")
159
+ ContentUnavailableView("Pick a folder", systemImage: "sidebar.leading")
194
160
  }
195
161
  }
196
162
  ```
197
163
 
198
- #### Tab-Based
164
+ Tab-based apps give each tab its own `NavigationStack` inside a `TabView`. The
165
+ full tab patterns are in `swiftui-navigation`.
199
166
 
200
- Use `TabView` with a `NavigationStack` per tab. See the `swiftui-navigation` skill for full tab patterns.
201
-
202
- #### Toolbar
167
+ Toolbars (`.topBarLeading` and `.topBarTrailing` come with the iOS 17 SDK and
168
+ are back-deployed to iOS 14):
203
169
 
204
170
  ```swift
205
171
  .toolbar {
206
- ToolbarItem(placement: .topBarLeading) { EditButton() }
172
+ ToolbarItem(placement: .topBarLeading) {
173
+ EditButton()
174
+ }
207
175
  ToolbarItemGroup(placement: .topBarTrailing) {
208
- Button("Filter", systemImage: "line.3.horizontal.decrease.circle") { }
209
- Button("Add", systemImage: "plus") { }
176
+ Button("Mark all read", systemImage: "envelope.open", action: markAllRead)
177
+ Button("New", systemImage: "plus", action: createItem)
210
178
  }
211
179
  ToolbarItemGroup(placement: .bottomBar) {
212
- Button("Archive", systemImage: "archivebox") { }
180
+ Button("Archive", systemImage: "archivebox", action: archiveSelection)
213
181
  Spacer()
214
- Text("\(itemCount) items").font(.footnote).foregroundStyle(.secondary)
182
+ Text("\(messages.count) messages")
183
+ .font(.footnote)
184
+ .foregroundStyle(.secondary)
215
185
  Spacer()
216
- Button("Share", systemImage: "square.and.arrow.up") { }
186
+ Button("Compose", systemImage: "square.and.pencil", action: compose)
217
187
  }
218
188
  }
219
189
  ```
220
190
 
221
- #### Search Integration
191
+ Search with scopes (`.searchScopes` needs iOS 16.4):
222
192
 
223
193
  ```swift
224
- .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
225
- .searchScopes($searchScope) {
226
- ForEach(SearchScope.allCases, id: \.self) { scope in
227
- Text(scope.rawValue.capitalized).tag(scope)
194
+ enum LibraryScope: String, CaseIterable {
195
+ case titles, authors, tags
196
+ var label: String { rawValue.localizedCapitalized }
197
+ }
198
+
199
+ .searchable(text: $query, placement: .navigationBarDrawer(displayMode: .always))
200
+ .searchScopes($scope) {
201
+ ForEach(LibraryScope.allCases, id: \.self) { scope in
202
+ Text(scope.label).tag(scope)
228
203
  }
229
204
  }
230
205
  ```
231
206
 
232
- ### Feedback
233
-
234
- #### Haptic Feedback
207
+ ## 5. HIG: Feedback
235
208
 
236
- Prefer SwiftUI's `sensoryFeedback(_:trigger:)` for state-driven feedback in SwiftUI views.
209
+ For haptics that follow state, use `sensoryFeedback(_:trigger:)` (iOS 17+):
237
210
 
238
211
  ```swift
239
- Button("Save") {
240
- didSave.toggle()
241
- }
242
- .sensoryFeedback(.success, trigger: didSave)
212
+ Button("Bookmark") { isBookmarked.toggle() }
213
+ .sensoryFeedback(.success, trigger: isBookmarked)
243
214
 
244
- Picker("Sort", selection: $sortOrder) {
245
- Text("Recent").tag(SortOrder.recent)
246
- Text("Popular").tag(SortOrder.popular)
215
+ Picker("Order", selection: $ordering) {
216
+ ForEach(Ordering.allCases) { Text($0.title).tag($0) }
247
217
  }
248
- .sensoryFeedback(.selection, trigger: sortOrder)
218
+ .sensoryFeedback(.selection, trigger: ordering)
249
219
  ```
250
220
 
251
- Use the UIKit generators only when you need imperative feedback from UIKit or non-SwiftUI integration points.
252
-
253
- See the Haptics section below for structured patterns.
221
+ Reach for UIKit feedback generators only when the trigger is imperative and
222
+ comes from UIKit or other code outside SwiftUI views.
254
223
 
255
- ### Accessibility
224
+ ## 6. HIG: Accessibility
256
225
 
257
- #### VoiceOver Support
226
+ Combine a row into one VoiceOver element and describe it:
258
227
 
259
228
  ```swift
260
229
  VStack(alignment: .leading) {
261
- Text(item.title).font(.headline)
262
- Text(item.subtitle).font(.subheadline).foregroundStyle(.secondary)
263
- HStack {
264
- Image(systemName: "star.fill")
265
- Text("\(item.rating, specifier: "%.1f")")
266
- }
230
+ Text(course.title).font(.headline)
231
+ Text("\(course.score, specifier: "%.1f") out of 5")
267
232
  }
268
233
  .accessibilityElement(children: .combine)
269
- .accessibilityLabel("\(item.title), \(item.subtitle)")
270
- .accessibilityValue("Rating: \(item.rating) stars")
271
- .accessibilityHint("Double tap to view details")
234
+ .accessibilityLabel(course.title)
235
+ .accessibilityValue("Rated \(course.score, specifier: "%.1f") out of 5")
236
+ .accessibilityHint("Opens the course overview")
272
237
  .accessibilityAddTraits(.isButton)
273
238
  ```
274
239
 
275
- #### Dynamic Type Support
276
-
277
- Adapt layout for accessibility sizes:
240
+ Switch layout for the accessibility text sizes:
278
241
 
279
242
  ```swift
280
- @Environment(\.dynamicTypeSize) private var dynamicTypeSize
243
+ @Environment(\.dynamicTypeSize) private var typeSize
281
244
 
282
245
  var body: some View {
283
- if dynamicTypeSize.isAccessibilitySize {
284
- VStack(alignment: .leading) {
285
- leadingContent
286
- trailingContent
287
- }
246
+ if typeSize.isAccessibilitySize {
247
+ VStack(alignment: .leading) { avatar; details }
288
248
  } else {
289
- HStack {
290
- leadingContent
291
- Spacer()
292
- trailingContent
293
- }
249
+ HStack { avatar; details; Spacer() }
294
250
  }
295
251
  }
296
252
  ```
297
253
 
298
- ### Error and Empty States
254
+ ## 7. HIG: Error and Empty States
299
255
 
300
- Use `ContentUnavailableView` for both:
256
+ `ContentUnavailableView` covers both. The builder form adds actions:
301
257
 
302
258
  ```swift
303
- // Error state
304
259
  ContentUnavailableView {
305
- Label("Unable to Load", systemImage: "exclamationmark.triangle")
260
+ Label("Sync failed", systemImage: "icloud.slash")
306
261
  } description: {
307
262
  Text(error.localizedDescription)
308
263
  } actions: {
309
- Button("Try Again") { Task { await retry() } }
264
+ Button("Retry") { Task { await resync() } }
310
265
  .buttonStyle(.borderedProminent)
311
266
  }
312
267
 
313
- // Empty state
314
268
  ContentUnavailableView {
315
- Label("No Photos", systemImage: "camera")
269
+ Label("No receipts", systemImage: "doc.text.viewfinder")
316
270
  } description: {
317
- Text("Take your first photo to get started.")
271
+ Text("Scan a paper receipt to add it here.")
318
272
  } actions: {
319
- Button("Take Photo") { showCamera = true }
273
+ Button("Scan") { isScannerPresented = true }
320
274
  .buttonStyle(.borderedProminent)
321
275
  }
322
276
  ```
323
277
 
324
- ## Theming and Dynamic Type
278
+ ## 8. Theming and Dynamic Type
325
279
 
326
- ### Intent
280
+ Goal: theming that grows with the app while view code stays semantic.
327
281
 
328
- Provide a clean, scalable theming approach that keeps view code semantic and consistent.
329
-
330
- ### Core patterns
331
-
332
- - Use a single `Theme` object as the source of truth (colors, fonts, spacing).
333
- - Inject theme at the app root and read it via `@Environment(Theme.self)` in views.
334
- - Prefer semantic colors (`primaryBackground`, `secondaryBackground`, `label`, `tint`) instead of raw colors.
335
- - Keep user-facing theme controls in a dedicated settings screen.
336
- - Apply Dynamic Type scaling through text styles, `Font.custom(_:size:relativeTo:)`, or `@ScaledMetric` for numeric layout values.
337
-
338
- ### Example: Theme object
282
+ - One `Theme` object is the only source of colors, fonts and spacing.
283
+ - Install it at the app root; views read it with `@Environment(Theme.self)`.
284
+ - Name values by role (`primaryBackground`, `secondaryBackground`, `label`,
285
+ `tint`) rather than by hue.
286
+ - Put user-facing theme choices on a dedicated settings screen.
287
+ - Scaling with the user's text size comes from three places: built-in text
288
+ styles, a custom font tied to a style via `Font.custom(_:size:relativeTo:)`,
289
+ and `@ScaledMetric` for numeric layout values.
339
290
 
340
291
  ```swift
341
- @MainActor
342
- @Observable
292
+ @MainActor @Observable
343
293
  final class Theme {
344
- var tintColor: Color = .blue
345
- var primaryBackground: Color = .white
346
- var secondaryBackground: Color = .gray.opacity(0.1)
347
- var labelColor: Color = .primary
348
- var fontSizeScale: Double = 1.0
294
+ var accent: Color = .teal
295
+ var primaryBackground: Color = .white
296
+ var secondaryBackground: Color = .secondary.opacity(0.12)
297
+ var label: Color = .primary
298
+ var textScale: Double = 1.0
349
299
  }
350
- ```
351
-
352
- ### Example: inject at app root
353
300
 
354
- ```swift
355
301
  @main
356
- struct MyApp: App {
357
- @State private var theme = Theme()
302
+ struct JournalApp: App {
303
+ @State private var theme = Theme()
358
304
 
359
- var body: some Scene {
360
- WindowGroup {
361
- AppView()
362
- .environment(theme)
305
+ var body: some Scene {
306
+ WindowGroup {
307
+ EntriesView().environment(theme)
308
+ }
363
309
  }
364
- }
365
310
  }
366
- ```
367
311
 
368
- ### Example: view usage
369
-
370
- ```swift
371
- struct ProfileView: View {
372
- @Environment(Theme.self) private var theme
312
+ struct EntryCard: View {
313
+ @Environment(Theme.self) private var currentTheme
314
+ let entry: Entry
373
315
 
374
- var body: some View {
375
- VStack {
376
- Text("Profile")
377
- .foregroundStyle(theme.labelColor)
316
+ var body: some View {
317
+ Text(entry.title)
318
+ .foregroundStyle(currentTheme.label)
319
+ .background(currentTheme.primaryBackground)
378
320
  }
379
- .background(theme.primaryBackground)
380
- }
381
321
  }
382
322
  ```
383
323
 
384
- ### Design choices to keep
324
+ Guidelines:
385
325
 
386
- - Keep theme values semantic and minimal; avoid duplicating system colors.
387
- - Store user-selected theme values in persistent storage if needed.
388
- - Ensure contrast between text and backgrounds.
326
+ - Keep the set of theme values small and semantic; do not re-create system
327
+ colors.
328
+ - Persist theme choices the user makes, if the app offers them.
329
+ - Check contrast between every text color and its background.
389
330
 
390
- ### Pitfalls
331
+ Pitfalls:
391
332
 
392
- - Avoid sprinkling raw `Color` values in views; it breaks consistency.
393
- - Do not tie theme to a single view's local state.
394
- - Avoid using `@Environment(\.colorScheme)` as the only theme control; it should complement your theme.
333
+ - Raw `Color` literals spread through views.
334
+ - A theme stored in one view's local state.
335
+ - Treating `@Environment(\.colorScheme)` as the whole theming story. Light or
336
+ dark mode is one input to the theme, not a substitute for it.
395
337
 
396
- ## Haptics
338
+ ## 9. Haptics
397
339
 
398
- ### Intent
340
+ Use haptics for moments that matter (tab switch, refresh, success, failure) and
341
+ respect the user's settings.
399
342
 
400
- Use haptics sparingly to reinforce user actions (tab selection, refresh, success/error) and respect user preferences.
343
+ - In SwiftUI views, prefer `sensoryFeedback(_:trigger:)`.
344
+ - Centralise imperative feedback in a manager only for UIKit interop or code
345
+ that is not a view.
346
+ - Check both the user preference and hardware support.
347
+ - Use different feedback types for different moments: selection, notification,
348
+ refresh.
349
+ - Core Haptics is the step up when a pattern needs shaping that the built-in
350
+ feedback types do not offer.
401
351
 
402
- ### Core patterns
403
-
404
- - Prefer `sensoryFeedback(_:trigger:)` in SwiftUI views for state-driven feedback.
405
- - Centralize imperative feedback in a `HapticManager` only when UIKit interop or non-view code requires it.
406
- - Gate haptics behind user preferences and hardware support.
407
- - Use distinct types for different UX moments (selection vs. notification vs. refresh).
408
- - Escalate to Core Haptics only for custom patterns that exceed SwiftUI's built-in feedback types.
409
-
410
- ### SwiftUI-first pattern
352
+ SwiftUI first:
411
353
 
412
354
  ```swift
413
- struct SaveButton: View {
414
- @State private var saveToken = 0
355
+ @State private var uploadCount = 0
415
356
 
416
- var body: some View {
417
- Button("Save") {
418
- persistChanges()
419
- saveToken += 1
420
- }
421
- .sensoryFeedback(.success, trigger: saveToken)
422
- }
357
+ Button("Upload") {
358
+ uploadCount += 1
359
+ startUpload()
423
360
  }
361
+ .sensoryFeedback(.success, trigger: uploadCount)
424
362
  ```
425
363
 
426
- ### UIKit interop pattern
364
+ UIKit interop:
427
365
 
428
366
  ```swift
367
+ import UIKit
368
+
429
369
  @MainActor
430
- final class HapticManager {
431
- static let shared = HapticManager()
432
-
433
- enum HapticType {
434
- case buttonPress
435
- case tabSelection
436
- case dataRefresh(intensity: CGFloat)
437
- case notification(UINotificationFeedbackGenerator.FeedbackType)
438
- }
439
-
440
- private let selectionGenerator = UISelectionFeedbackGenerator()
441
- private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
442
- private let notificationGenerator = UINotificationFeedbackGenerator()
443
-
444
- private init() { selectionGenerator.prepare() }
445
-
446
- func fire(_ type: HapticType, isEnabled: Bool) {
447
- guard isEnabled else { return }
448
- switch type {
449
- case .buttonPress:
450
- impactGenerator.impactOccurred()
451
- case .tabSelection:
452
- selectionGenerator.selectionChanged()
453
- case let .dataRefresh(intensity):
454
- impactGenerator.impactOccurred(intensity: intensity)
455
- case let .notification(style):
456
- notificationGenerator.notificationOccurred(style)
370
+ final class FeedbackPlayer {
371
+ static let shared = FeedbackPlayer()
372
+
373
+ enum Kind {
374
+ case tap
375
+ case tabChange
376
+ case refresh(strength: CGFloat)
377
+ case result(UINotificationFeedbackGenerator.FeedbackType)
378
+ }
379
+
380
+ private let selection = UISelectionFeedbackGenerator()
381
+ private let impact = UIImpactFeedbackGenerator(style: .heavy)
382
+ private let notification = UINotificationFeedbackGenerator()
383
+
384
+ private init() { selection.prepare() }
385
+
386
+ func play(_ kind: Kind, allowed: Bool) {
387
+ guard allowed else { return }
388
+ switch kind {
389
+ case .tap: impact.impactOccurred()
390
+ case .tabChange: selection.selectionChanged()
391
+ case .refresh(let strength): impact.impactOccurred(intensity: strength)
392
+ case .result(let type): notification.notificationOccurred(type)
393
+ }
457
394
  }
458
- }
459
395
  }
460
396
  ```
461
397
 
462
- ### Example: usage
463
-
464
398
  ```swift
465
- Button("Save") {
466
- HapticManager.shared.fire(.notification(.success), isEnabled: preferences.hapticsEnabled)
399
+ @AppStorage("hapticsOn") private var hapticsOn = true
400
+
401
+ Button("Pay") {
402
+ FeedbackPlayer.shared.play(.result(.success), allowed: hapticsOn)
467
403
  }
468
404
 
469
- TabView(selection: $selectedTab) { /* tabs */ }
470
- .onChange(of: selectedTab) { _, _ in
471
- HapticManager.shared.fire(.tabSelection, isEnabled: preferences.hapticTabSelectionEnabled)
472
- }
405
+ TabView(selection: $tab) { tabs }
406
+ .onChange(of: tab) {
407
+ FeedbackPlayer.shared.play(.tabChange, allowed: hapticsOn)
408
+ }
473
409
  ```
474
410
 
475
- ### Design choices to keep
476
-
477
- - Haptics should be subtle and not fire on every tiny interaction.
478
- - Respect user preferences (toggle to disable).
479
- - Keep haptic triggers close to the user action, not deep in data layers.
411
+ Guidelines:
480
412
 
481
- ### Pitfalls
413
+ - Keep feedback subtle and skip it for minor interactions.
414
+ - Offer a setting that turns haptics off.
415
+ - Trigger feedback where the user acts, not from the data layer.
482
416
 
483
- - Avoid firing multiple haptics in quick succession.
484
- - Do not assume haptics are available; check support.
417
+ Pitfalls:
485
418
 
486
- ### Core Haptics (CHHapticEngine)
419
+ - Several haptics fired back to back.
420
+ - Assuming the device has a haptic engine.
487
421
 
488
- For advanced haptic patterns beyond the simple feedback generators, use Core Haptics. It provides precise control over haptic intensity, sharpness, and timing with support for audio-haptic synchronization.
422
+ ## 10. Core Haptics
489
423
 
490
- > **Docs:** [CHHapticEngine](https://sosumi.ai/documentation/corehaptics/chhapticengine) · [Preparing your app to play haptics](https://sosumi.ai/documentation/corehaptics/preparing-your-app-to-play-haptics)
424
+ Core Haptics controls intensity, sharpness, timing and audio sync precisely,
425
+ for patterns the simple generators cannot produce. Apple docs:
426
+ [CHHapticEngine](https://developer.apple.com/documentation/corehaptics/chhapticengine),
427
+ and the [Core Haptics](https://developer.apple.com/documentation/corehaptics)
428
+ topic page, whose "Essentials" group holds the article on preparing an app to
429
+ play haptics.
491
430
 
492
- #### Capabilities check
431
+ ### Engine lifecycle
493
432
 
494
- Always verify hardware support before creating an engine:
433
+ Check the hardware before creating an engine.
495
434
 
496
435
  ```swift
497
436
  import CoreHaptics
437
+ import OSLog
498
438
 
499
- let supportsHaptics = CHHapticEngine.capabilitiesForHardware().supportsHaptics
500
- let supportsAudio = CHHapticEngine.capabilitiesForHardware().supportsAudio
501
- ```
502
-
503
- #### Engine setup and lifecycle
439
+ extension Logger {
440
+ static let haptics = Logger(subsystem: "app.haptics", category: "engine")
441
+ }
504
442
 
505
- ```swift
506
443
  @MainActor
507
- final class CoreHapticManager {
444
+ final class PatternPlayer {
508
445
  private var engine: CHHapticEngine?
509
446
 
510
- func prepareEngine() throws {
511
- guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
447
+ var canPlayHaptics: Bool {
448
+ CHHapticEngine.capabilitiesForHardware().supportsHaptics
449
+ }
512
450
 
513
- engine = try CHHapticEngine()
451
+ var canPlayHapticAudio: Bool {
452
+ CHHapticEngine.capabilitiesForHardware().supportsAudio
453
+ }
514
454
 
515
- // Called when the engine stops due to external cause (audio session interruption, app backgrounding)
516
- engine?.stoppedHandler = { reason in
517
- print("Haptic engine stopped: \(reason)")
455
+ func startEngine() throws {
456
+ guard canPlayHaptics else { return }
457
+ let engine = try CHHapticEngine()
458
+ engine.stoppedHandler = { @Sendable reason in
459
+ Logger.haptics.info("Engine stopped, reason \(reason.rawValue)")
518
460
  }
519
-
520
- // Called after the engine is reset (e.g., after audio session interruption ends)
521
- engine?.resetHandler = { [weak self] in
522
- do {
523
- try self?.engine?.start()
524
- } catch {
525
- print("Failed to restart engine: \(error)")
526
- }
461
+ engine.resetHandler = { @Sendable [weak self] in
462
+ Task { @MainActor in try? self?.engine?.start() }
527
463
  }
528
-
529
- try engine?.start()
464
+ try engine.start()
465
+ self.engine = engine
530
466
  }
531
467
 
532
468
  func stopEngine() {
@@ -535,304 +471,316 @@ final class CoreHapticManager {
535
471
  }
536
472
  ```
537
473
 
538
- Key lifecycle rules:
539
- - Call `engine.start()` before playing any patterns.
540
- - Handle `stoppedHandler` - the system can stop the engine when your app moves to the background or during audio interruptions.
541
- - Handle `resetHandler` - restart the engine when the system resets it.
542
- - Call `engine.stop()` when haptics are no longer needed to save battery.
543
-
544
- #### CHHapticPattern and CHHapticEvent
474
+ - `stoppedHandler` runs when something outside the app stops the engine, such
475
+ as an audio session interruption or the app moving to the background.
476
+ - Both handlers are called on a queue owned by the engine, so they are
477
+ `@Sendable`; any state change is scheduled onto the main actor first.
478
+ - `resetHandler` runs after the system resets the engine, for instance once an
479
+ audio interruption is over. Start the engine again there.
480
+ - Call `start()` before playing any pattern.
481
+ - Once the screen no longer plays patterns, `stop()` the engine to save power.
545
482
 
546
- Build patterns from individual haptic and audio events:
483
+ ### A single tap
547
484
 
548
485
  ```swift
549
- func playTransientTap() throws {
550
- let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
551
- let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
552
-
553
- // Transient: short, single-tap feel
554
- let event = CHHapticEvent(
555
- eventType: .hapticTransient,
556
- parameters: [intensity, sharpness],
557
- relativeTime: 0
558
- )
559
-
560
- let pattern = try CHHapticPattern(events: [event], parameters: [])
561
- let player = try engine?.makePlayer(with: pattern)
562
- try player?.start(atTime: CHHapticTimeImmediate)
486
+ func playTap() throws {
487
+ let crisp = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.75)
488
+ let strong = CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.95)
489
+ let tap = CHHapticEvent(eventType: .hapticTransient,
490
+ parameters: [crisp, strong],
491
+ relativeTime: 0)
492
+ let pattern = try CHHapticPattern(events: [tap], parameters: [])
493
+ try engine?.makePlayer(with: pattern).start(atTime: CHHapticTimeImmediate)
563
494
  }
564
495
  ```
565
496
 
566
- **Event types:**
497
+ ### Event types and parameters
567
498
 
568
- | Type | Description |
569
- |------|-------------|
570
- | `.hapticTransient` | Brief, tap-like impulse |
571
- | `.hapticContinuous` | Sustained vibration over a `duration` |
499
+ | Event type | Effect |
500
+ |---|---|
501
+ | `.hapticTransient` | Short impulse, like a tap |
502
+ | `.hapticContinuous` | Sustained vibration for a `duration` |
572
503
  | `.audioContinuous` | Sustained audio tone |
573
- | `.audioCustom` | Play a custom audio resource |
574
-
575
- **Common parameters:** `.hapticIntensity` (0-1), `.hapticSharpness` (0-1), `.attackTime`, `.decayTime`, `.releaseTime`.
504
+ | `.audioCustom` | Plays an audio resource you register |
576
505
 
577
- #### Playing patterns with CHHapticPatternPlayer
506
+ Common parameters: `.hapticIntensity` (0 to 1), `.hapticSharpness` (0 to 1),
507
+ `.attackTime`, `.decayTime`, `.releaseTime`.
578
508
 
579
509
  ```swift
580
- func playContinuousBuzz() throws {
581
- let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6)
582
- let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
583
-
584
- let event = CHHapticEvent(
510
+ func playHum() throws {
511
+ let hum = CHHapticEvent(
585
512
  eventType: .hapticContinuous,
586
- parameters: [intensity, sharpness],
513
+ parameters: [
514
+ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.55),
515
+ CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.35)
516
+ ],
587
517
  relativeTime: 0,
588
518
  duration: 0.5
589
519
  )
590
-
591
- let pattern = try CHHapticPattern(events: [event], parameters: [])
592
- let player = try engine?.makePlayer(with: pattern)
593
- try player?.start(atTime: CHHapticTimeImmediate)
520
+ let humPattern = try CHHapticPattern(events: [hum], parameters: [])
521
+ try engine?.makePlayer(with: humPattern).start(atTime: CHHapticTimeImmediate)
594
522
  }
595
523
  ```
596
524
 
597
- For looping, seeking, and pausing, use `CHHapticAdvancedPatternPlayer` via `engine.makeAdvancedPlayer(with:)`.
525
+ Looping, seeking and pausing need a `CHHapticAdvancedPatternPlayer` from
526
+ `engine.makeAdvancedPlayer(with:)`.
598
527
 
599
- #### Haptic parameter curves (CHHapticParameterCurve)
528
+ ### Parameter curves
600
529
 
601
- Smoothly vary parameters over time within a pattern:
530
+ A `CHHapticParameterCurve` changes a parameter smoothly while the pattern
531
+ plays:
602
532
 
603
533
  ```swift
604
- func playRampingPattern() throws {
605
- let event = CHHapticEvent(
534
+ func playSwell() throws {
535
+ let base = CHHapticEvent(
606
536
  eventType: .hapticContinuous,
607
537
  parameters: [
608
- CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
609
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
538
+ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.25),
539
+ CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15)
610
540
  ],
611
541
  relativeTime: 0,
612
542
  duration: 1.0
613
543
  )
614
-
615
- // Ramp intensity from 0.2 → 1.0 over 1 second
616
- let curve = CHHapticParameterCurve(
544
+ let swell = CHHapticParameterCurve(
617
545
  parameterID: .hapticIntensityControl,
618
546
  controlPoints: [
619
- .init(relativeTime: 0, value: 0.2),
620
- .init(relativeTime: 0.5, value: 0.7),
621
- .init(relativeTime: 1.0, value: 1.0)
547
+ CHHapticParameterCurve.ControlPoint(relativeTime: 0, value: 0.25),
548
+ CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.6),
549
+ CHHapticParameterCurve.ControlPoint(relativeTime: 0.9, value: 1)
622
550
  ],
623
551
  relativeTime: 0
624
552
  )
625
-
626
- let pattern = try CHHapticPattern(events: [event], parameterCurves: [curve])
627
- let player = try engine?.makePlayer(with: pattern)
628
- try player?.start(atTime: CHHapticTimeImmediate)
553
+ let pattern = try CHHapticPattern(events: [base], parameterCurves: [swell])
554
+ try engine?.makePlayer(with: pattern).start(atTime: CHHapticTimeImmediate)
629
555
  }
630
556
  ```
631
557
 
632
- #### Audio-haptic synchronization (AHAP files)
558
+ ### AHAP files
633
559
 
634
- AHAP (Apple Haptic and Audio Pattern) files define haptic patterns in JSON for easy authoring and design iteration. Load them directly:
560
+ Patterns can also live outside code, in an AHAP file: a JSON document you
561
+ tune without recompiling.
635
562
 
636
563
  ```swift
637
- func playAHAPFile() throws {
638
- guard let url = Bundle.main.url(forResource: "success", withExtension: "ahap") else { return }
639
- try engine?.playPattern(from: url)
564
+ func playFromFile() throws {
565
+ guard let patternURL = Bundle.main.url(forResource: "checkout", withExtension: "ahap") else {
566
+ return
567
+ }
568
+ try engine?.playPattern(from: patternURL)
640
569
  }
641
570
  ```
642
571
 
643
- AHAP files support the same events, parameters, and parameter curves as the programmatic API. Use the **Core Haptics** design tools in Xcode to preview patterns.
644
-
645
- > **Docs:** [Representing haptic patterns in AHAP files](https://sosumi.ai/documentation/corehaptics/representing-haptic-patterns-in-ahap-files)
646
-
647
- ## Matched Transitions
572
+ Everything the code API expresses (events, parameters, curves) can be written
573
+ in AHAP too, and Xcode can play the file back for you while you iterate. File
574
+ format reference: the AHAP article listed on the
575
+ [Core Haptics](https://developer.apple.com/documentation/corehaptics) topic page.
648
576
 
649
- ### Intent
577
+ ## 11. Matched Transitions
650
578
 
651
- Use matched transitions to create smooth continuity between a source view (thumbnail, avatar) and a destination view (sheet, detail, viewer).
579
+ Goal: carry the eye from a source view (thumbnail, avatar) to its destination
580
+ (sheet, detail, full-screen viewer).
652
581
 
653
- ### Core patterns
654
-
655
- - Use a shared `Namespace` and a stable ID for the source.
656
- - Use `matchedTransitionSource` + `navigationTransition(.zoom(...))` on iOS 26+.
657
- - Use `matchedGeometryEffect` for in-place transitions within a view hierarchy.
658
- - Keep IDs stable across view updates (avoid random UUIDs).
659
-
660
- ### Example: media preview to full-screen viewer (iOS 26+)
582
+ - Share a `Namespace` between source and destination, and give the source a
583
+ stable id.
584
+ - Across screens, use `matchedTransitionSource(id:in:)` with
585
+ `navigationTransition(.zoom(sourceID:in:))`. Both need iOS 18.
586
+ - Inside one hierarchy, use `matchedGeometryEffect(id:in:)`.
587
+ - Ids must stay the same across updates; never generate random UUIDs for them.
661
588
 
662
589
  ```swift
663
- struct MediaPreview: View {
664
- @Namespace private var namespace
665
- @State private var selected: MediaAttachment?
666
-
667
- var body: some View {
668
- ThumbnailView()
669
- .matchedTransitionSource(id: selected?.id ?? "", in: namespace)
670
- .sheet(item: $selected) { item in
671
- MediaViewer(item: item)
672
- .navigationTransition(.zoom(sourceID: item.id, in: namespace))
673
- }
674
- }
590
+ struct GalleryGrid: View {
591
+ @Namespace private var zoom
592
+ @State private var opened: Photo?
593
+ let photos: [Photo]
594
+
595
+ var body: some View {
596
+ LazyVGrid(columns: [GridItem(.adaptive(minimum: 96))]) {
597
+ ForEach(photos) { photo in
598
+ Button { opened = photo } label: {
599
+ PhotoThumbnail(photo: photo)
600
+ }
601
+ .matchedTransitionSource(id: photo.id, in: zoom)
602
+ }
603
+ }
604
+ .sheet(item: $opened) { photo in
605
+ PhotoViewer(photo: photo)
606
+ .navigationTransition(.zoom(sourceID: photo.id, in: zoom))
607
+ }
608
+ }
675
609
  }
676
610
  ```
677
611
 
678
- ### Example: matched geometry within a view
612
+ In-place change within one view:
679
613
 
680
614
  ```swift
681
- struct ToggleBadge: View {
682
- @Namespace private var space
683
- @State private var isOn = false
684
-
685
- var body: some View {
686
- Button {
687
- withAnimation(.spring) { isOn.toggle() }
688
- } label: {
689
- Image(systemName: isOn ? "eye" : "eye.slash")
690
- .matchedGeometryEffect(id: "icon", in: space)
615
+ struct LockToggle: View {
616
+ @Namespace private var space
617
+ @State private var isLocked = false
618
+
619
+ var body: some View {
620
+ Button {
621
+ withAnimation(.spring) { isLocked.toggle() }
622
+ } label: {
623
+ Image(systemName: isLocked ? "lock" : "lock.open")
624
+ .matchedGeometryEffect(id: "lockGlyph", in: space)
625
+ }
691
626
  }
692
- }
693
627
  }
694
628
  ```
695
629
 
696
- ### Design choices to keep
697
-
698
- - Prefer `matchedTransitionSource` for cross-screen transitions.
699
- - Keep source and destination sizes reasonable to avoid jarring scale changes.
700
- - Use `withAnimation` for state-driven transitions.
701
-
702
- ### Pitfalls
630
+ Guidelines:
703
631
 
704
- - Don't use unstable IDs; it breaks the transition.
705
- - Avoid mismatched shapes (e.g., square to circle) unless the design expects it.
632
+ - Prefer `matchedTransitionSource` for transitions between screens.
633
+ - Keep source and destination sizes close so the scale change is not jarring.
634
+ - Drive state-based transitions with `withAnimation`.
706
635
 
707
- ## Loading and Placeholders
636
+ Pitfalls:
708
637
 
709
- Use this when a view needs a consistent loading state (skeletons, redaction, empty state) without blocking interaction.
638
+ - Ids that change between updates break the transition.
639
+ - Shapes that do not match (a square into a circle) look wrong unless the
640
+ design asks for it.
710
641
 
711
- ### Patterns to prefer
642
+ ## 12. Loading and Placeholders
712
643
 
713
- - **Redacted placeholders** for list/detail content to preserve layout while loading.
714
- - **ContentUnavailableView** for empty or error states after loading completes.
715
- - **ProgressView** only for short, global operations (use sparingly in content-heavy screens).
644
+ Use this for loading states (skeletons, redaction, empty state) that stay
645
+ consistent and never block interaction.
716
646
 
717
- ### Recommended approach
647
+ - For lists and detail content, prefer redacted placeholders; the layout stays
648
+ in place while data arrives.
649
+ - Once loading ends with nothing to show, or with an error, show
650
+ `ContentUnavailableView`.
651
+ - Keep `ProgressView` for short, global operations, and use it sparingly on
652
+ content-heavy screens.
718
653
 
719
- 1. Keep the real layout, render placeholder data, then apply `.redacted(reason: .placeholder)`.
720
- 2. For lists, show a fixed number of placeholder rows (avoid infinite spinners).
721
- 3. Switch to `ContentUnavailableView` when load finishes but data is empty.
654
+ Steps:
722
655
 
723
- ### Pitfalls
724
-
725
- - Don't animate layout shifts during redaction; keep frames stable.
726
- - Avoid nesting multiple spinners; use one loading indicator per section.
727
- - Keep placeholder count small (3-6) to reduce jank on low-end devices.
728
-
729
- ### Minimal usage
656
+ 1. Render the real layout with placeholder data and apply
657
+ `.redacted(reason: .placeholder)`.
658
+ 2. A list gets a small, fixed set of skeleton rows, never a spinner that runs
659
+ with no end in sight.
660
+ 3. When loading finishes with no data, switch to `ContentUnavailableView`.
730
661
 
731
662
  ```swift
732
- VStack {
733
- if isLoading {
734
- ForEach(0..<3, id: \.self) { _ in
735
- RowView(model: .placeholder())
663
+ List {
664
+ if isFetching {
665
+ ForEach(0..<4, id: \.self) { _ in
666
+ InvoiceRow(invoice: .sample)
667
+ .redacted(reason: .placeholder)
668
+ }
669
+ } else if invoices.isEmpty {
670
+ ContentUnavailableView("No invoices", systemImage: "tray")
671
+ } else {
672
+ ForEach(invoices) { InvoiceRow(invoice: $0) }
736
673
  }
737
- .redacted(reason: .placeholder)
738
- } else if items.isEmpty {
739
- ContentUnavailableView("No items", systemImage: "tray")
740
- } else {
741
- ForEach(items) { item in RowView(model: item) }
742
- }
743
674
  }
744
675
  ```
745
676
 
746
- ## Focus Handling
677
+ Pitfalls and limits:
747
678
 
748
- This file covers basic form-focus patterns only. For directional focus, focus sections, scene-focused values, and `UIFocusGuide`, see the `focus-engine` skill.
679
+ - Layout that shifts or animates while redacted. Keep frames stable.
680
+ - Spinners nested inside spinners. One indicator per section.
681
+ - More than three to six placeholder rows, which costs frames on older
682
+ devices.
749
683
 
750
- ### Intent
684
+ ## 13. Form Focus
751
685
 
752
- Use `@FocusState` to control keyboard focus, chain fields, and coordinate focus across complex forms.
686
+ This section covers keyboard focus in forms. Directional focus, focus sections
687
+ and focus on tvOS, macOS and iPadOS keyboards are covered in `hig-inputs`;
688
+ VoiceOver and other assistive focus is covered in `ios-accessibility`.
753
689
 
754
- ### Core patterns
690
+ `@FocusState` sets keyboard focus, moves it from field to field, and
691
+ coordinates it across a complex form.
755
692
 
756
- - Use an enum to represent focusable fields.
757
- - Set initial focus in `onAppear`.
758
- - Use `.onSubmit` to move focus to the next field.
759
- - For dynamic lists of fields, use an enum with associated values (e.g., `.option(Int)`).
693
+ - Describe the focusable fields with an enum.
694
+ - Set the first focus in `onAppear`.
695
+ - Move to the following field in `.onSubmit`.
696
+ - For a variable number of fields, use an enum case with an associated value,
697
+ such as `.answer(Int)`.
760
698
 
761
- ### Example: single field focus
699
+ One field:
762
700
 
763
701
  ```swift
764
- struct AddServerView: View {
765
- @State private var server = ""
766
- @FocusState private var isServerFieldFocused: Bool
767
-
768
- var body: some View {
769
- Form {
770
- TextField("Server", text: $server)
771
- .focused($isServerFieldFocused)
702
+ struct HostEntryForm: View {
703
+ @State private var host = ""
704
+ @FocusState private var hostFocused: Bool
705
+
706
+ var body: some View {
707
+ Form {
708
+ TextField("Host", text: $host)
709
+ .focused($hostFocused)
710
+ }
711
+ .onAppear { hostFocused = true }
772
712
  }
773
- .onAppear { isServerFieldFocused = true }
774
- }
775
713
  }
776
714
  ```
777
715
 
778
- ### Example: chained focus with enum
716
+ Chained fields:
779
717
 
780
718
  ```swift
781
- struct EditTagView: View {
782
- enum FocusField { case title, symbol, newTag }
783
- @FocusState private var focusedField: FocusField?
784
-
785
- var body: some View {
786
- Form {
787
- TextField("Title", text: $title)
788
- .focused($focusedField, equals: .title)
789
- .onSubmit { focusedField = .symbol }
790
-
791
- TextField("Symbol", text: $symbol)
792
- .focused($focusedField, equals: .symbol)
793
- .onSubmit { focusedField = .newTag }
719
+ struct ContactForm: View {
720
+ enum Field { case name, phone, note }
721
+
722
+ @State private var name = ""
723
+ @State private var phone = ""
724
+ @State private var note = ""
725
+ @FocusState private var focus: Field?
726
+
727
+ var body: some View {
728
+ Form {
729
+ TextField("Name", text: $name)
730
+ .focused($focus, equals: .name)
731
+ .onSubmit { focus = .phone }
732
+ TextField("Phone", text: $phone)
733
+ .focused($focus, equals: .phone)
734
+ .onSubmit { focus = .note }
735
+ TextField("Note", text: $note)
736
+ .focused($focus, equals: .note)
737
+ }
738
+ .onAppear { focus = .name }
794
739
  }
795
- .onAppear { focusedField = .title }
796
- }
797
740
  }
798
741
  ```
799
742
 
800
- ### Example: dynamic focus for variable fields
743
+ A growing list of fields:
801
744
 
802
745
  ```swift
803
- struct PollView: View {
804
- enum FocusField: Hashable { case option(Int) }
805
- @FocusState private var focused: FocusField?
806
- @State private var options: [String] = ["", ""]
807
- @State private var currentIndex = 0
808
-
809
- var body: some View {
810
- ForEach(options.indices, id: \.self) { index in
811
- TextField("Option \(index + 1)", text: $options[index])
812
- .focused($focused, equals: .option(index))
813
- .onSubmit { addOption(at: index) }
746
+ struct SurveyAnswers: View {
747
+ enum Field: Hashable { case answer(Int) }
748
+
749
+ @State private var answers = [""]
750
+ @FocusState private var focus: Field?
751
+
752
+ var body: some View {
753
+ Form {
754
+ ForEach(answers.indices, id: \.self) { index in
755
+ TextField("Answer \(index + 1)", text: $answers[index])
756
+ .focused($focus, equals: .answer(index))
757
+ .onSubmit { addAnswer(after: index) }
758
+ }
759
+ }
814
760
  }
815
- .onAppear { focused = .option(0) }
816
- }
817
-
818
- private func addOption(at index: Int) {
819
- options.append("")
820
- currentIndex = index + 1
821
- Task { @MainActor in
822
- try? await Task.sleep(for: .milliseconds(10))
823
- focused = .option(currentIndex)
761
+
762
+ private func addAnswer(after index: Int) {
763
+ answers.append("")
764
+ let next = answers.count - 1
765
+ Task { @MainActor in
766
+ try? await Task.sleep(for: Duration.milliseconds(15))
767
+ focus = .answer(next)
768
+ }
824
769
  }
825
- }
826
770
  }
827
771
  ```
828
772
 
829
- ### Design choices to keep
773
+ The short sleep lets SwiftUI insert the new field before focus moves to it.
774
+
775
+ Guidelines:
830
776
 
831
- - Keep focus state local to the view that owns the fields.
832
- - Use focus changes to drive UX (validation messages, helper UI).
833
- - Pair with `.scrollDismissesKeyboard(...)` when using ScrollView/Form.
777
+ - The view holding the text fields also holds their `@FocusState`.
778
+ - React to focus moving: show a validation message or a hint for the field
779
+ that just gained or lost focus.
780
+ - Inside a `ScrollView` or `Form`, pair focus with `.scrollDismissesKeyboard(_:)`.
834
781
 
835
- ### Pitfalls
782
+ Pitfalls:
836
783
 
837
- - Don't store focus state in shared objects; it is view-local.
838
- - Avoid aggressive focus changes during animation; delay if needed.
784
+ - Focus state stored in shared objects; it is local to the view.
785
+ - Focus changed aggressively in the middle of an animation. Delay it when
786
+ needed.