@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,774 +1,530 @@
1
- # Camera Capture
1
+ # Capture session recipes
2
2
 
3
- Complete patterns for AVCaptureSession setup, photo capture, video recording, and camera features in SwiftUI. All patterns use a dedicated `@Observable` model that owns the session; the SwiftUI view only displays the preview and triggers actions.
4
-
5
- ---
3
+ Shape used throughout: an `@Observable` model owns the capture session and all
4
+ camera state; the SwiftUI view draws the preview and forwards taps. Session
5
+ work runs on one private serial queue, because `AVCaptureSession` is not
6
+ `Sendable` and its start and stop calls block.
6
7
 
7
8
  ## Contents
8
9
 
9
- - [1. Complete Camera Manager with Photo Capture](#1-complete-camera-manager-with-photo-capture)
10
- - [2. Camera Preview (UIViewRepresentable)](#2-camera-preview-uiviewrepresentable)
11
- - [3. Complete Camera Screen in SwiftUI](#3-complete-camera-screen-in-swiftui)
12
- - [4. Video Recording](#4-video-recording)
13
- - [5. Flash and Torch Control](#5-flash-and-torch-control)
14
- - [6. Focus and Exposure](#6-focus-and-exposure)
15
- - [7. Barcode and QR Code Scanning](#7-barcode-and-qr-code-scanning)
16
- - [8. Camera Preview Orientation](#8-camera-preview-orientation)
17
- - [9. Dual Camera and Device Discovery](#9-dual-camera-and-device-discovery)
18
- - [10. Restricting Scan Region](#10-restricting-scan-region)
10
+ - [1. A full photo camera](#1-a-full-photo-camera)
11
+ - [2. Preview view](#2-preview-view)
12
+ - [3. Camera screen](#3-camera-screen)
13
+ - [4. Recording video](#4-recording-video)
14
+ - [5. Flash and torch](#5-flash-and-torch)
15
+ - [6. Tap to focus and expose](#6-tap-to-focus-and-expose)
16
+ - [7. Barcodes and QR codes](#7-barcodes-and-qr-codes)
17
+ - [8. Orientation](#8-orientation)
18
+ - [9. Choosing specific cameras](#9-choosing-specific-cameras)
19
+ - [10. Limiting the scan area](#10-limiting-the-scan-area)
19
20
 
20
- ## 1. Complete Camera Manager with Photo Capture
21
+ ## 1. A full photo camera
21
22
 
22
- A full-featured camera model with photo capture using the delegate pattern.
23
+ The rig owns the session graph and does all session work on its queue. The
24
+ model is main-actor state plus the capture delegate, which is why it inherits
25
+ from `NSObject`.
23
26
 
24
27
  ```swift
25
28
  import AVFoundation
26
- import UIKit
29
+ import Observation
27
30
 
28
- @available(iOS 17.0, *)
29
- @Observable
30
- @MainActor
31
- final class CameraManager: NSObject {
31
+ final class CameraRig: @unchecked Sendable {
32
32
  let session = AVCaptureSession()
33
+ let stills = AVCapturePhotoOutput()
34
+ private(set) var activeInput: AVCaptureDeviceInput?
35
+ private let queue = DispatchQueue(label: "camera.rig")
33
36
 
34
- private let photoOutput = AVCapturePhotoOutput()
35
- private var currentInput: AVCaptureDeviceInput?
36
- private var photoContinuation: CheckedContinuation<Data?, Never>?
37
-
38
- var isRunning = false
39
- var cameraPosition: AVCaptureDevice.Position = .back
40
- var flashMode: AVCaptureDevice.FlashMode = .auto
41
- var lastCapturedPhoto: Data?
42
- var error: String?
43
-
44
- // MARK: - Configuration
45
-
46
- func configure() async {
47
- guard await requestAccess() else {
48
- error = "Camera access denied"
49
- return
37
+ func onQueue<T: Sendable>(_ work: @escaping @Sendable () -> T) async -> T {
38
+ await withCheckedContinuation { done in
39
+ queue.async { done.resume(returning: work()) }
50
40
  }
41
+ }
51
42
 
43
+ func assemble(facing side: AVCaptureDevice.Position) -> String? {
52
44
  session.beginConfiguration()
45
+ defer { session.commitConfiguration() }
53
46
  session.sessionPreset = .photo
47
+ guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: side),
48
+ let feedIn = try? AVCaptureDeviceInput(device: camera),
49
+ session.canAddInput(feedIn) else { return "No usable camera" }
50
+ session.addInput(feedIn)
51
+ activeInput = feedIn
52
+ guard session.canAddOutput(stills) else { return "Photo output rejected" }
53
+ session.addOutput(stills)
54
+ let largest = camera.activeFormat.supportedMaxPhotoDimensions
55
+ .max { Int($0.width) * Int($0.height) < Int($1.width) * Int($1.height) }
56
+ if let largest { stills.maxPhotoDimensions = largest }
57
+ stills.maxPhotoQualityPrioritization = .quality
58
+ return nil
59
+ }
60
+
61
+ func swapInput(to side: AVCaptureDevice.Position) -> Bool {
62
+ guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: side),
63
+ let replacement = try? AVCaptureDeviceInput(device: camera) else { return false }
64
+ session.beginConfiguration()
65
+ defer { session.commitConfiguration() }
66
+ if let activeInput { session.removeInput(activeInput) }
67
+ guard session.canAddInput(replacement) else { return false }
68
+ session.addInput(replacement)
69
+ activeInput = replacement
70
+ return true
71
+ }
72
+ }
54
73
 
55
- // Add camera input
56
- guard let device = cameraDevice(for: cameraPosition),
57
- let input = try? AVCaptureDeviceInput(device: device),
58
- session.canAddInput(input) else {
59
- error = "Failed to configure camera input"
60
- session.commitConfiguration()
61
- return
62
- }
63
- session.addInput(input)
64
- currentInput = input
65
-
66
- // Add photo output
67
- guard session.canAddOutput(photoOutput) else {
68
- error = "Failed to configure photo output"
69
- session.commitConfiguration()
70
- return
71
- }
72
- session.addOutput(photoOutput)
74
+ @available(iOS 17.0, *)
75
+ @Observable @MainActor
76
+ final class StudioCamera: NSObject {
77
+ private(set) var isRunning = false
78
+ private(set) var facing: AVCaptureDevice.Position = .back
79
+ var flash: AVCaptureDevice.FlashMode = .auto
80
+ private(set) var latestShot: Data?
81
+ private(set) var problem: String?
73
82
 
74
- // Enable maximum quality (iOS 16+)
75
- if let maxDimensions = photoOutput.maxPhotoDimensions(for: .photo) {
76
- photoOutput.maxPhotoDimensions = maxDimensions
77
- }
78
- photoOutput.maxPhotoQualityPrioritization = .quality
83
+ @ObservationIgnored let rig = CameraRig()
84
+ @ObservationIgnored private var pendingShot: CheckedContinuation<Data?, Never>?
79
85
 
80
- session.commitConfiguration()
86
+ func configure() async {
87
+ guard await CameraAccess.isGranted() else { problem = "Camera access denied"; return }
88
+ let side = facing
89
+ problem = await rig.onQueue { [rig] in rig.assemble(facing: side) }
81
90
  }
82
91
 
83
- // MARK: - Session Control
84
-
85
- func start() {
86
- guard !session.isRunning else { return }
87
- Task.detached { [session] in
88
- session.startRunning()
89
- }
92
+ func start() async {
93
+ guard !isRunning else { return }
94
+ await rig.onQueue { [rig] in rig.session.startRunning() }
90
95
  isRunning = true
91
96
  }
92
97
 
93
- func stop() {
94
- guard session.isRunning else { return }
95
- Task.detached { [session] in
96
- session.stopRunning()
97
- }
98
+ func stop() async {
99
+ guard isRunning else { return }
100
+ await rig.onQueue { [rig] in rig.session.stopRunning() }
98
101
  isRunning = false
99
102
  }
100
103
 
101
- // MARK: - Photo Capture
102
-
103
- func capturePhoto() async -> Data? {
104
- let settings = AVCapturePhotoSettings()
105
- settings.flashMode = flashMode
106
-
107
- if photoOutput.availablePhotoCodecTypes.contains(.hevc) {
108
- settings.photoQualityPrioritization = .quality
109
- }
110
-
111
- return await withCheckedContinuation { continuation in
112
- photoContinuation = continuation
113
- photoOutput.capturePhoto(with: settings, delegate: self)
114
- }
104
+ func flipCamera() async {
105
+ let target: AVCaptureDevice.Position = facing == .back ? .front : .back
106
+ if await rig.onQueue({ [rig] in rig.swapInput(to: target) }) { facing = target }
115
107
  }
116
108
 
117
- // MARK: - Camera Switching
118
-
119
- func switchCamera() {
120
- let newPosition: AVCaptureDevice.Position = (cameraPosition == .back) ? .front : .back
121
-
122
- guard let device = cameraDevice(for: newPosition),
123
- let newInput = try? AVCaptureDeviceInput(device: device) else { return }
124
-
125
- session.beginConfiguration()
126
- if let currentInput {
127
- session.removeInput(currentInput)
128
- }
129
- if session.canAddInput(newInput) {
130
- session.addInput(newInput)
131
- currentInput = newInput
132
- cameraPosition = newPosition
133
- }
134
- session.commitConfiguration()
135
- }
136
-
137
- // MARK: - Helpers
138
-
139
- private func cameraDevice(for position: AVCaptureDevice.Position) -> AVCaptureDevice? {
140
- AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position)
141
- }
142
-
143
- private func requestAccess() async -> Bool {
144
- let status = AVCaptureDevice.authorizationStatus(for: .video)
145
- if status == .notDetermined {
146
- return await AVCaptureDevice.requestAccess(for: .video)
109
+ func capturePhoto() async -> Data? {
110
+ guard pendingShot == nil else { return nil }
111
+ let mode = flash
112
+ return await withCheckedContinuation { waiter in
113
+ pendingShot = waiter
114
+ let output = rig.stills
115
+ let settings = AVCapturePhotoSettings()
116
+ settings.flashMode = mode
117
+ if output.availablePhotoCodecTypes.contains(.hevc) {
118
+ settings.photoQualityPrioritization = .quality
119
+ }
120
+ output.capturePhoto(with: settings, delegate: self)
147
121
  }
148
- return status == .authorized
149
122
  }
150
123
  }
151
124
 
152
- // MARK: - AVCapturePhotoCaptureDelegate
153
-
154
125
  @available(iOS 17.0, *)
155
- extension CameraManager: AVCapturePhotoCaptureDelegate {
156
- nonisolated func photoOutput(
157
- _ output: AVCapturePhotoOutput,
158
- didFinishProcessingPhoto photo: AVCapturePhoto,
159
- error: Error?
160
- ) {
161
- let data = photo.fileDataRepresentation()
126
+ extension StudioCamera: AVCapturePhotoCaptureDelegate {
127
+ nonisolated func photoOutput(_ sender: AVCapturePhotoOutput,
128
+ didFinishProcessingPhoto photo: AVCapturePhoto,
129
+ error: Error?) {
130
+ let encoded = photo.fileDataRepresentation()
162
131
  Task { @MainActor in
163
- lastCapturedPhoto = data
164
- photoContinuation?.resume(returning: data)
165
- photoContinuation = nil
132
+ latestShot = encoded
133
+ pendingShot?.resume(returning: encoded)
134
+ pendingShot = nil
166
135
  }
167
136
  }
168
137
  }
169
138
  ```
170
139
 
171
- The `capturePhoto()` method bridges the delegate-based API to async/await using `CheckedContinuation`. Store only one continuation at a time -- overlapping captures are not supported.
140
+ Points worth keeping:
172
141
 
173
- ---
142
+ - If any configuration step fails, record the message and still commit the
143
+ configuration; the `defer` above does that.
144
+ - `maxPhotoDimensions` (iOS 16+) must be one of the active format's
145
+ `supportedMaxPhotoDimensions`; the largest gives full-resolution stills.
146
+ Set `maxPhotoQualityPrioritization` before capturing, since a per-shot
147
+ prioritization cannot exceed it.
148
+ - Only one shot may be in flight: the model stores a single continuation and
149
+ refuses a second request until the first resumes.
150
+ - Switching cameras removes the current input and adds the new one inside a
151
+ single configuration block.
152
+ - The session is never captured by a detached task from the main actor. Swift
153
+ 6 rejects sending a main-actor-owned non-`Sendable` value that way; the rig's
154
+ queue is the single place the session is touched.
174
155
 
175
- ## 2. Camera Preview (UIViewRepresentable)
156
+ ## 2. Preview view
176
157
 
177
158
  ```swift
178
- import SwiftUI
179
- import AVFoundation
180
-
181
- struct CameraPreview: UIViewRepresentable {
159
+ struct CameraSurface: UIViewRepresentable {
182
160
  let session: AVCaptureSession
161
+ var onFocusTap: ((CGPoint, AVCaptureVideoPreviewLayer) -> Void)?
183
162
 
184
- func makeUIView(context: Context) -> CameraPreviewView {
185
- let view = CameraPreviewView()
186
- view.previewLayer.session = session
187
- view.previewLayer.videoGravity = .resizeAspectFill
188
- return view
163
+ final class Surface: UIView {
164
+ override static var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
165
+ var feed: AVCaptureVideoPreviewLayer { unsafeDowncast(layer, to: AVCaptureVideoPreviewLayer.self) }
189
166
  }
190
167
 
191
- func updateUIView(_ uiView: CameraPreviewView, context: Context) {
192
- if uiView.previewLayer.session !== session {
193
- uiView.previewLayer.session = session
194
- }
168
+ func makeUIView(context: Context) -> Surface {
169
+ let surface = Surface()
170
+ surface.feed.session = session
171
+ surface.feed.videoGravity = .resizeAspectFill
172
+ let relay = context.coordinator
173
+ surface.addGestureRecognizer(UITapGestureRecognizer(target: relay, action: #selector(Coordinator.tapped(_:))))
174
+ return surface
195
175
  }
196
- }
197
176
 
198
- /// Custom UIView that uses AVCaptureVideoPreviewLayer as its backing layer.
199
- /// Overriding layerClass ensures the preview layer resizes automatically with the view.
200
- final class CameraPreviewView: UIView {
201
- override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
177
+ func updateUIView(_ surface: Surface, context: Context) {
178
+ context.coordinator.onFocusTap = onFocusTap
179
+ }
180
+
181
+ func makeCoordinator() -> Coordinator { Coordinator() }
182
+
183
+ @MainActor
184
+ final class Coordinator: NSObject {
185
+ var onFocusTap: ((CGPoint, AVCaptureVideoPreviewLayer) -> Void)?
202
186
 
203
- var previewLayer: AVCaptureVideoPreviewLayer {
204
- layer as! AVCaptureVideoPreviewLayer
187
+ @objc func tapped(_ gesture: UITapGestureRecognizer) {
188
+ guard let surface = gesture.view as? Surface else { return }
189
+ onFocusTap?(gesture.location(in: surface), surface.feed)
190
+ }
205
191
  }
206
192
  }
207
193
  ```
208
194
 
209
- Never add `AVCaptureVideoPreviewLayer` as a sublayer manually. Using `layerClass` avoids manual frame management in `layoutSubviews`.
210
-
211
- ---
195
+ With `layerClass` returning the preview layer class, the preview is the view's
196
+ own layer and resizes with it. Do not insert an `AVCaptureVideoPreviewLayer`
197
+ as a sublayer and then chase its frame in `layoutSubviews`.
198
+ The coordinator is `@MainActor`: the tap arrives on the main thread, and
199
+ reading `gesture.view` or the surface's layer from a nonisolated class is a
200
+ Swift 6 isolation warning.
212
201
 
213
- ## 3. Complete Camera Screen in SwiftUI
202
+ ## 3. Camera screen
214
203
 
215
204
  ```swift
216
- import SwiftUI
217
-
218
205
  @available(iOS 17.0, *)
219
- struct CameraScreen: View {
220
- @State private var camera = CameraManager()
221
- @State private var showCapturedPhoto = false
206
+ struct StudioScreen: View {
207
+ @State private var camera = StudioCamera()
208
+ @State private var reviewing = false
222
209
 
223
210
  var body: some View {
224
211
  ZStack {
225
- CameraPreview(session: camera.session)
226
- .ignoresSafeArea()
227
-
212
+ CameraSurface(session: camera.rig.session).ignoresSafeArea()
228
213
  VStack {
229
- // Top controls
230
214
  HStack {
231
- Button {
232
- camera.switchCamera()
233
- } label: {
234
- Image(systemName: "camera.rotate")
235
- .font(.title2)
236
- .padding()
237
- .background(.ultraThinMaterial, in: Circle())
238
- }
239
-
215
+ roundButton("camera.rotate") { Task { await camera.flipCamera() } }
240
216
  Spacer()
241
-
242
- // Flash toggle
243
- Button {
244
- camera.flashMode = (camera.flashMode == .off) ? .auto : .off
245
- } label: {
246
- Image(systemName: camera.flashMode == .off
247
- ? "bolt.slash.fill" : "bolt.fill")
248
- .font(.title2)
249
- .padding()
250
- .background(.ultraThinMaterial, in: Circle())
217
+ roundButton(camera.flash == .auto ? "bolt.fill" : "bolt.slash.fill") {
218
+ camera.flash = camera.flash == .off ? .auto : .off
251
219
  }
252
220
  }
253
- .padding(.horizontal)
254
-
255
221
  Spacer()
256
-
257
- // Bottom controls
258
222
  HStack {
259
- // Thumbnail of last capture
260
- if let data = camera.lastCapturedPhoto,
261
- let uiImage = UIImage(data: data) {
262
- Button { showCapturedPhoto = true } label: {
263
- Image(uiImage: uiImage)
264
- .resizable()
265
- .scaledToFill()
266
- .frame(width: 50, height: 50)
267
- .clipShape(.rect(cornerRadius: 8))
268
- }
269
- } else {
270
- Color.clear.frame(width: 50, height: 50)
271
- }
272
-
223
+ thumbnail.onTapGesture { reviewing = camera.latestShot != nil }
273
224
  Spacer()
274
-
275
- // Shutter button
276
- Button {
277
- Task { _ = await camera.capturePhoto() }
278
- } label: {
279
- ZStack {
280
- Circle().fill(.white).frame(width: 72, height: 72)
281
- Circle().stroke(.gray, lineWidth: 3).frame(width: 78, height: 78)
282
- }
225
+ Button(action: shoot) {
226
+ Circle().frame(width: 72, height: 72).foregroundStyle(.white)
227
+ .overlay { Circle().strokeBorder(lineWidth: 3).foregroundStyle(.white).frame(width: 78, height: 78) }
283
228
  }
284
-
285
229
  Spacer()
286
-
287
- Color.clear.frame(width: 50, height: 50)
230
+ Color.clear.frame(width: 50)
288
231
  }
289
- .padding(.horizontal)
290
- .padding(.bottom)
291
232
  }
233
+ .padding()
292
234
  }
293
235
  .task {
294
236
  await camera.configure()
295
- camera.start()
237
+ await camera.start()
296
238
  }
297
- .onDisappear {
298
- camera.stop()
239
+ .onDisappear { Task { await camera.stop() } }
240
+ .sheet(isPresented: $reviewing) {
241
+ if let shot = camera.latestShot, let image = UIImage(data: shot) {
242
+ Image(uiImage: image).resizable().scaledToFit()
243
+ }
299
244
  }
300
- .sheet(isPresented: $showCapturedPhoto) {
301
- if let data = camera.lastCapturedPhoto,
302
- let uiImage = UIImage(data: data) {
303
- Image(uiImage: uiImage)
304
- .resizable()
305
- .scaledToFit()
245
+ }
246
+
247
+ private func shoot() {
248
+ Task { _ = await camera.capturePhoto() }
249
+ }
250
+
251
+ private var thumbnail: some View {
252
+ Group {
253
+ if let shot = camera.latestShot, let image = UIImage(data: shot) {
254
+ Image(uiImage: image).resizable().scaledToFill()
255
+ } else {
256
+ Color.gray.opacity(0.3)
306
257
  }
307
258
  }
259
+ .frame(width: 50, height: 50)
260
+ .clipShape(.rect(cornerRadius: 8))
261
+ }
262
+
263
+ private func roundButton(_ symbol: String, action: @escaping () -> Void) -> some View {
264
+ Button(action: action) {
265
+ Image(systemName: symbol).padding(12).background(.ultraThinMaterial, in: Circle())
266
+ }
308
267
  }
309
268
  }
310
269
  ```
311
270
 
312
- ---
271
+ ## 4. Recording video
313
272
 
314
- ## 4. Video Recording
315
-
316
- Add `AVCaptureMovieFileOutput` for video capture. Video recording requires `NSMicrophoneUsageDescription` in Info.plist for audio.
273
+ Movies go through `AVCaptureMovieFileOutput`. Sound needs
274
+ `NSMicrophoneUsageDescription`.
317
275
 
318
276
  ```swift
319
- import AVFoundation
320
-
321
- @available(iOS 17.0, *)
322
- @Observable
323
- @MainActor
324
- final class VideoRecorder: NSObject {
277
+ final class MovieRig: @unchecked Sendable {
325
278
  let session = AVCaptureSession()
279
+ let movies = AVCaptureMovieFileOutput()
280
+ private let queue = DispatchQueue(label: "movie.rig")
326
281
 
327
- private let movieOutput = AVCaptureMovieFileOutput()
328
- private var videoContinuation: CheckedContinuation<URL?, Never>?
329
-
330
- var isRecording = false
331
- var recordedVideoURL: URL?
332
- var error: String?
333
-
334
- func configure() async {
335
- guard await requestAccess() else {
336
- error = "Camera access denied"
337
- return
338
- }
282
+ func onQueue<T: Sendable>(_ work: @escaping @Sendable () -> T) async -> T {
283
+ await withCheckedContinuation { done in queue.async { done.resume(returning: work()) } }
284
+ }
339
285
 
286
+ func assemble() -> Bool {
340
287
  session.beginConfiguration()
288
+ defer { session.commitConfiguration() }
341
289
  session.sessionPreset = .high
342
-
343
- // Video input
344
- guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera,
345
- for: .video, position: .back),
346
- let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
347
- session.canAddInput(videoInput) else {
348
- session.commitConfiguration()
349
- return
350
- }
351
- session.addInput(videoInput)
352
-
353
- // Audio input
354
- if let audioDevice = AVCaptureDevice.default(for: .audio),
355
- let audioInput = try? AVCaptureDeviceInput(device: audioDevice),
356
- session.canAddInput(audioInput) {
357
- session.addInput(audioInput)
358
- }
359
-
360
- // Movie output
361
- guard session.canAddOutput(movieOutput) else {
362
- session.commitConfiguration()
363
- return
364
- }
365
- session.addOutput(movieOutput)
366
-
367
- session.commitConfiguration()
368
- }
369
-
370
- func startRecording() {
371
- guard !isRecording else { return }
372
-
373
- let outputURL = FileManager.default.temporaryDirectory
374
- .appendingPathComponent(UUID().uuidString)
375
- .appendingPathExtension("mov")
376
-
377
- movieOutput.startRecording(to: outputURL, recordingDelegate: self)
378
- isRecording = true
290
+ guard let lens = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .back),
291
+ let picture = try? AVCaptureDeviceInput(device: lens),
292
+ session.canAddInput(picture) else { return false }
293
+ session.addInput(picture)
294
+ if let mic = AVCaptureDevice.default(for: .audio),
295
+ let sound = try? AVCaptureDeviceInput(device: mic),
296
+ session.canAddInput(sound) {
297
+ session.addInput(sound)
298
+ }
299
+ guard session.canAddOutput(movies) else { return false }
300
+ session.addOutput(movies)
301
+ return true
379
302
  }
303
+ }
380
304
 
381
- func stopRecording() async -> URL? {
382
- guard isRecording else { return nil }
305
+ @available(iOS 17.0, *)
306
+ @Observable @MainActor
307
+ final class ClipRecorder: NSObject {
308
+ private(set) var isRecording = false
309
+ private(set) var hasMicrophone = true
310
+ @ObservationIgnored let rig = MovieRig()
311
+ @ObservationIgnored private var finishing: CheckedContinuation<URL?, Never>?
383
312
 
384
- return await withCheckedContinuation { continuation in
385
- videoContinuation = continuation
386
- movieOutput.stopRecording()
313
+ func prepare() async -> Bool {
314
+ if AVCaptureDevice.authorizationStatus(for: .audio) == .notDetermined {
315
+ hasMicrophone = await AVCaptureDevice.requestAccess(for: .audio)
387
316
  }
317
+ guard await CameraAccess.isGranted() else { return false }
318
+ return await rig.onQueue { [rig] in rig.assemble() }
388
319
  }
389
320
 
390
- func start() {
391
- Task.detached { [session] in session.startRunning() }
392
- }
393
-
394
- func stop() {
395
- Task.detached { [session] in session.stopRunning() }
321
+ func beginRecording() {
322
+ let target = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).mov")
323
+ rig.movies.startRecording(to: target, recordingDelegate: self)
324
+ isRecording = true
396
325
  }
397
326
 
398
- private func requestAccess() async -> Bool {
399
- let videoStatus = AVCaptureDevice.authorizationStatus(for: .video)
400
- let videoGranted: Bool
401
- if videoStatus == .notDetermined {
402
- videoGranted = await AVCaptureDevice.requestAccess(for: .video)
403
- } else {
404
- videoGranted = videoStatus == .authorized
405
- }
406
-
407
- // Also request audio for video recording
408
- let audioStatus = AVCaptureDevice.authorizationStatus(for: .audio)
409
- if audioStatus == .notDetermined {
410
- _ = await AVCaptureDevice.requestAccess(for: .audio)
327
+ func endRecording() async -> URL? {
328
+ await withCheckedContinuation { waiter in
329
+ finishing = waiter
330
+ rig.movies.stopRecording()
411
331
  }
412
-
413
- return videoGranted
414
332
  }
415
333
  }
416
334
 
417
335
  @available(iOS 17.0, *)
418
- extension VideoRecorder: AVCaptureFileOutputRecordingDelegate {
419
- nonisolated func fileOutput(
420
- _ output: AVCaptureFileOutput,
421
- didFinishRecordingTo outputFileURL: URL,
422
- from connections: [AVCaptureConnection],
423
- error: Error?
424
- ) {
336
+ extension ClipRecorder: AVCaptureFileOutputRecordingDelegate {
337
+ nonisolated func fileOutput(_ recorder: AVCaptureFileOutput,
338
+ didFinishRecordingTo outputFileURL: URL,
339
+ from connections: [AVCaptureConnection],
340
+ error: Error?) {
341
+ let finished = error == nil ? outputFileURL : nil
425
342
  Task { @MainActor in
426
343
  isRecording = false
427
- recordedVideoURL = error == nil ? outputFileURL : nil
428
- videoContinuation?.resume(returning: error == nil ? outputFileURL : nil)
429
- videoContinuation = nil
344
+ finishing?.resume(returning: finished)
345
+ finishing = nil
430
346
  }
431
347
  }
432
348
  }
433
349
  ```
434
350
 
435
- Clean up temporary video files when they are no longer needed. Recorded videos can be large and the temporary directory is not automatically cleaned during the app's lifetime.
436
-
437
- ---
351
+ Microphone access is requested alongside the camera, but only the camera answer
352
+ decides whether recording can go ahead. Delete temporary movies as soon as they
353
+ are no longer needed. Movie files are large, and nothing clears that folder
354
+ for you while the app is alive.
438
355
 
439
- ## 5. Flash and Torch Control
356
+ ## 5. Flash and torch
440
357
 
441
- Flash applies to photo capture settings. Torch provides continuous illumination for video or preview.
358
+ Flash is a per-shot setting on `AVCapturePhotoSettings`. The torch is a steady
359
+ light for video or for the preview.
442
360
 
443
361
  ```swift
444
- import AVFoundation
445
-
446
- func toggleTorch(on device: AVCaptureDevice, enabled: Bool) throws {
447
- guard device.hasTorch, device.isTorchAvailable else { return }
448
-
362
+ func setLamp(_ on: Bool, on device: AVCaptureDevice) throws {
363
+ guard device.isTorchAvailable, device.hasTorch else { return }
449
364
  try device.lockForConfiguration()
450
- device.torchMode = enabled ? .on : .off
451
- if enabled {
452
- try device.setTorchModeOn(level: AVCaptureDevice.maxAvailableTorchLevel)
365
+ defer { device.unlockForConfiguration() }
366
+ if on {
367
+ let brightest = AVCaptureDevice.maxAvailableTorchLevel
368
+ try device.setTorchModeOn(level: brightest)
369
+ } else {
370
+ device.torchMode = .off
453
371
  }
454
- device.unlockForConfiguration()
455
372
  }
456
373
  ```
457
374
 
458
- Always wrap device configuration in `lockForConfiguration()` / `unlockForConfiguration()`. Multiple clients may attempt to configure the device simultaneously.
459
-
460
- ---
375
+ Wrap every device change in `lockForConfiguration()` and
376
+ `unlockForConfiguration()`; other clients can be configuring the same device.
461
377
 
462
- ## 6. Focus and Exposure
463
-
464
- Implement tap-to-focus by converting a SwiftUI tap location to the camera coordinate system.
378
+ ## 6. Tap to focus and expose
465
379
 
466
380
  ```swift
467
- import AVFoundation
468
-
469
- func setFocusAndExposure(
470
- at point: CGPoint,
471
- in previewLayer: AVCaptureVideoPreviewLayer,
472
- device: AVCaptureDevice
473
- ) throws {
474
- let devicePoint = previewLayer.captureDevicePointConverted(fromLayerPoint: point)
475
-
381
+ func focusAndExpose(at viewPoint: CGPoint, in feed: AVCaptureVideoPreviewLayer, device: AVCaptureDevice) throws {
382
+ let devicePoint = feed.captureDevicePointConverted(fromLayerPoint: viewPoint)
476
383
  try device.lockForConfiguration()
477
-
384
+ defer { device.unlockForConfiguration() }
478
385
  if device.isFocusPointOfInterestSupported {
479
386
  device.focusPointOfInterest = devicePoint
480
387
  device.focusMode = .autoFocus
481
388
  }
482
-
483
389
  if device.isExposurePointOfInterestSupported {
484
390
  device.exposurePointOfInterest = devicePoint
485
391
  device.exposureMode = .autoExpose
486
392
  }
487
-
488
- device.unlockForConfiguration()
489
393
  }
490
394
  ```
491
395
 
492
- ### Integrating Tap-to-Focus in SwiftUI
493
-
494
- ```swift
495
- struct FocusableCameraPreview: UIViewRepresentable {
496
- let session: AVCaptureSession
497
- var onTapToFocus: ((CGPoint, AVCaptureVideoPreviewLayer) -> Void)?
498
-
499
- func makeUIView(context: Context) -> CameraPreviewView {
500
- let view = CameraPreviewView()
501
- view.previewLayer.session = session
502
- view.previewLayer.videoGravity = .resizeAspectFill
396
+ `CameraSurface` above installs a `UITapGestureRecognizer` whose `@objc`
397
+ handler converts `gesture.location(in:)` and passes it, with the preview layer,
398
+ to the `onFocusTap` closure.
503
399
 
504
- let tap = UITapGestureRecognizer(
505
- target: context.coordinator,
506
- action: #selector(Coordinator.handleTap(_:))
507
- )
508
- view.addGestureRecognizer(tap)
509
-
510
- return view
511
- }
512
-
513
- func updateUIView(_ uiView: CameraPreviewView, context: Context) {}
514
-
515
- func makeCoordinator() -> Coordinator { Coordinator(self) }
516
-
517
- final class Coordinator: NSObject {
518
- var parent: FocusableCameraPreview
519
-
520
- init(_ parent: FocusableCameraPreview) { self.parent = parent }
521
-
522
- @objc func handleTap(_ gesture: UITapGestureRecognizer) {
523
- guard let view = gesture.view as? CameraPreviewView else { return }
524
- let point = gesture.location(in: view)
525
- parent.onTapToFocus?(point, view.previewLayer)
526
- }
527
- }
528
- }
529
- ```
530
-
531
- ---
532
-
533
- ## 7. Barcode and QR Code Scanning
534
-
535
- Use `AVCaptureMetadataOutput` to detect barcodes and QR codes from the camera feed.
400
+ ## 7. Barcodes and QR codes
536
401
 
537
402
  ```swift
538
- import AVFoundation
539
-
540
403
  @available(iOS 17.0, *)
541
- @Observable
542
- @MainActor
543
- final class QRCodeScanner: NSObject {
544
- let session = AVCaptureSession()
545
-
546
- private let metadataOutput = AVCaptureMetadataOutput()
547
-
548
- var scannedCode: String?
549
- var isScanning = false
550
-
551
- func configure() async {
552
- guard await requestAccess() else { return }
553
-
404
+ @Observable @MainActor
405
+ final class TicketScanner: NSObject, AVCaptureMetadataOutputObjectsDelegate {
406
+ private(set) var payload: String?
407
+ private(set) var isScanning = false
408
+ @ObservationIgnored let rig = CameraRig()
409
+ @ObservationIgnored private let codes = AVCaptureMetadataOutput()
410
+
411
+ func configure() {
412
+ let session = rig.session
554
413
  session.beginConfiguration()
555
-
556
- guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
557
- for: .video, position: .back),
558
- let input = try? AVCaptureDeviceInput(device: device),
559
- session.canAddInput(input) else {
560
- session.commitConfiguration()
561
- return
562
- }
414
+ defer { session.commitConfiguration() }
415
+ guard let lens = AVCaptureDevice.default(for: .video),
416
+ let input = try? AVCaptureDeviceInput(device: lens),
417
+ session.canAddInput(input), session.canAddOutput(codes) else { return }
563
418
  session.addInput(input)
564
-
565
- guard session.canAddOutput(metadataOutput) else {
566
- session.commitConfiguration()
567
- return
568
- }
569
- session.addOutput(metadataOutput)
570
-
571
- // Set metadata types AFTER adding to session -- available types depend on session config
572
- metadataOutput.metadataObjectTypes = [.qr, .ean8, .ean13, .code128, .code39]
573
- metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
574
-
575
- session.commitConfiguration()
419
+ session.addOutput(codes)
420
+ codes.metadataObjectTypes = [.qr, .ean13, .ean8, .code39, .code128]
421
+ codes.setMetadataObjectsDelegate(self, queue: .main)
576
422
  }
577
423
 
578
- func start() {
579
- scannedCode = nil
424
+ func start() async {
425
+ payload = nil
580
426
  isScanning = true
581
- Task.detached { [session] in session.startRunning() }
427
+ await rig.onQueue { [rig] in rig.session.startRunning() }
582
428
  }
583
429
 
584
- func stop() {
585
- isScanning = false
586
- Task.detached { [session] in session.stopRunning() }
587
- }
588
-
589
- private func requestAccess() async -> Bool {
590
- let status = AVCaptureDevice.authorizationStatus(for: .video)
591
- if status == .notDetermined {
592
- return await AVCaptureDevice.requestAccess(for: .video)
593
- }
594
- return status == .authorized
595
- }
596
- }
597
-
598
- @available(iOS 17.0, *)
599
- extension QRCodeScanner: AVCaptureMetadataOutputObjectsDelegate {
600
- nonisolated func metadataOutput(
601
- _ output: AVCaptureMetadataOutput,
602
- didOutput metadataObjects: [AVMetadataObject],
603
- from connection: AVCaptureConnection
604
- ) {
605
- guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
606
- let value = object.stringValue else { return }
607
-
430
+ nonisolated func metadataOutput(_ reader: AVCaptureMetadataOutput,
431
+ didOutput metadataObjects: [AVMetadataObject],
432
+ from connection: AVCaptureConnection) {
433
+ guard let code = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
434
+ let text = code.stringValue else { return }
608
435
  Task { @MainActor in
609
- scannedCode = value
610
- stop()
611
- }
612
- }
613
- }
614
- ```
615
-
616
- ### Scanner View
617
-
618
- ```swift
619
- @available(iOS 17.0, *)
620
- struct QRScannerView: View {
621
- @State private var scanner = QRCodeScanner()
622
-
623
- var body: some View {
624
- ZStack {
625
- CameraPreview(session: scanner.session)
626
- .ignoresSafeArea()
627
-
628
- // Scanning overlay
629
- RoundedRectangle(cornerRadius: 12)
630
- .stroke(.white, lineWidth: 2)
631
- .frame(width: 250, height: 250)
632
-
633
- if let code = scanner.scannedCode {
634
- VStack {
635
- Spacer()
636
- Text(code)
637
- .font(.headline)
638
- .padding()
639
- .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
640
- .padding(.bottom)
641
- }
642
- }
643
- }
644
- .task {
645
- await scanner.configure()
646
- scanner.start()
647
- }
648
- .onDisappear {
649
- scanner.stop()
436
+ payload = text
437
+ isScanning = false
438
+ await rig.onQueue { [rig] in rig.session.stopRunning() }
650
439
  }
651
440
  }
652
441
  }
653
442
  ```
654
443
 
655
- Set `metadataObjectTypes` after adding the output to the session. Setting types before causes a runtime crash because the available types are not yet determined.
656
-
657
- ---
444
+ Building this small graph on the main actor is quick; starting and stopping
445
+ still go through the rig's queue. Set `metadataObjectTypes` only after the
446
+ output is part of the session. Before
447
+ that the output knows no available types and assigning them crashes.
658
448
 
659
- ## 8. Camera Preview Orientation
449
+ A scanner screen is the preview with a 250 by 250 rounded rectangle stroked in
450
+ white over the center, plus the decoded text on a material background once
451
+ `payload` is set.
660
452
 
661
- Handle device rotation so the preview and captured photos have correct orientation.
453
+ ## 8. Orientation
662
454
 
663
- ### Preview Layer Rotation
455
+ On iOS 17 and later, rotation is an angle on the connection. `videoOrientation`
456
+ is deprecated.
664
457
 
665
458
  ```swift
666
- import AVFoundation
667
- import UIKit
668
-
669
- func updatePreviewOrientation(
670
- _ previewLayer: AVCaptureVideoPreviewLayer,
671
- for interfaceOrientation: UIInterfaceOrientation
672
- ) {
673
- guard let connection = previewLayer.connection else { return }
674
-
675
- // iOS 17+: use videoRotationAngle
676
- if #available(iOS 17.0, *) {
677
- let angle: CGFloat
678
- switch interfaceOrientation {
679
- case .portrait: angle = 90
680
- case .portraitUpsideDown: angle = 270
681
- case .landscapeLeft: angle = 180
682
- case .landscapeRight: angle = 0
683
- default: angle = 90
684
- }
685
- if connection.isVideoRotationAngleSupported(angle) {
686
- connection.videoRotationAngle = angle
687
- }
688
- }
689
- }
690
- ```
691
-
692
- ### Photo Output Orientation
693
-
694
- Set the rotation angle on the photo output connection before each capture to ensure the captured image matches the device orientation:
695
-
696
- ```swift
697
- func capturePhotoWithOrientation() {
698
- if let connection = photoOutput.connection(with: .video) {
699
- // iOS 17+
700
- if #available(iOS 17.0, *) {
701
- let angle = currentVideoRotationAngle()
702
- if connection.isVideoRotationAngleSupported(angle) {
703
- connection.videoRotationAngle = angle
704
- }
705
- }
459
+ @MainActor
460
+ func currentRotationAngle() -> CGFloat {
461
+ let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first
462
+ switch scene?.effectiveGeometry.interfaceOrientation {
463
+ case .landscapeRight: return 0
464
+ case .landscapeLeft: return 180
465
+ case .portraitUpsideDown: return 270
466
+ default: return 90
706
467
  }
707
-
708
- let settings = AVCapturePhotoSettings()
709
- photoOutput.capturePhoto(with: settings, delegate: self)
710
468
  }
711
469
 
712
- private func currentVideoRotationAngle() -> CGFloat {
713
- guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
714
- return 90
470
+ @MainActor
471
+ func applyRotation(preview: AVCaptureVideoPreviewLayer, stills: AVCapturePhotoOutput) {
472
+ let angle = currentRotationAngle()
473
+ if let feed = preview.connection, feed.isVideoRotationAngleSupported(angle) {
474
+ feed.videoRotationAngle = angle
715
475
  }
716
- switch scene.interfaceOrientation {
717
- case .portrait: return 90
718
- case .portraitUpsideDown: return 270
719
- case .landscapeLeft: return 180
720
- case .landscapeRight: return 0
721
- default: return 90
476
+ if let still = stills.connection(with: .video), still.isVideoRotationAngleSupported(angle) {
477
+ still.videoRotationAngle = angle
722
478
  }
723
479
  }
724
480
  ```
725
481
 
726
- Use `videoRotationAngle` (iOS 17+) instead of the deprecated `videoOrientation` property. The angle is measured in degrees clockwise from landscape-right (the natural sensor orientation).
482
+ - Read the orientation from `effectiveGeometry.interfaceOrientation` (iOS 16).
483
+ `UIWindowScene.interfaceOrientation` is deprecated as of iOS 26.
484
+ - The angle is in degrees, clockwise, counted from landscape-right, which is
485
+ the sensor's native orientation. Only 0, 90, 180 and 270 are accepted.
486
+ - Update the photo output's connection before each shot so the saved image
487
+ matches how the device was held.
488
+ - `AVCaptureDeviceRotationCoordinator` (iOS 17) can supply the preview and
489
+ capture angles for a device and preview layer, and tracks changes for you.
727
490
 
728
- ---
729
-
730
- ## 9. Dual Camera and Device Discovery
731
-
732
- Select specific camera hardware using `AVCaptureDevice.DiscoverySession`.
491
+ ## 9. Choosing specific cameras
733
492
 
734
493
  ```swift
735
- import AVFoundation
736
-
737
- func availableCameras() -> [AVCaptureDevice] {
738
- let discoverySession = AVCaptureDevice.DiscoverySession(
739
- deviceTypes: [
740
- .builtInWideAngleCamera,
741
- .builtInUltraWideCamera,
742
- .builtInTelephotoCamera,
743
- .builtInDualCamera,
744
- .builtInTripleCamera
745
- ],
494
+ func allCameras() -> [AVCaptureDevice] {
495
+ AVCaptureDevice.DiscoverySession(
496
+ deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera, .builtInTelephotoCamera,
497
+ .builtInDualCamera, .builtInTripleCamera],
746
498
  mediaType: .video,
747
499
  position: .unspecified
748
- )
749
- return discoverySession.devices
500
+ ).devices
750
501
  }
751
502
 
752
- func preferredBackCamera() -> AVCaptureDevice? {
753
- // Prefer triple > dual > wide angle
754
- let session = AVCaptureDevice.DiscoverySession(
503
+ func bestRearCamera() -> AVCaptureDevice? {
504
+ AVCaptureDevice.DiscoverySession(
755
505
  deviceTypes: [.builtInTripleCamera, .builtInDualCamera, .builtInWideAngleCamera],
756
506
  mediaType: .video,
757
507
  position: .back
758
- )
759
- return session.devices.first
508
+ ).devices.first
760
509
  }
761
510
  ```
762
511
 
763
- ---
512
+ `.unspecified` returns cameras on both sides. The order of `deviceTypes` is the
513
+ preference order, so the rear query prefers triple, then dual, then wide.
764
514
 
765
- ## 10. Restricting Scan Region
515
+ ## 10. Limiting the scan area
766
516
 
767
- Limit the metadata detection area to improve performance and UX:
517
+ `rectOfInterest` on the metadata output shrinks the searched region, which is
518
+ faster and keeps stray codes out. A rectangle with origin (0.2, 0.2) and size
519
+ 0.6 by 0.6 covers the middle 60 percent.
768
520
 
769
521
  ```swift
770
- // Restrict detection to center 60% of the preview
771
- metadataOutput.rectOfInterest = CGRect(x: 0.2, y: 0.2, width: 0.6, height: 0.6)
522
+ @MainActor
523
+ func restrictScan(_ codes: AVCaptureMetadataOutput, to box: CGRect, in feed: AVCaptureVideoPreviewLayer) {
524
+ codes.rectOfInterest = feed.metadataOutputRectConverted(fromLayerRect: box)
525
+ }
772
526
  ```
773
527
 
774
- Note that `rectOfInterest` uses the camera coordinate system (landscape, origin top-left). Convert from preview coordinates using `previewLayer.metadataOutputRectConverted(fromLayerRect:)`.
528
+ The rectangle lives in normalized camera coordinates: landscape, origin at the
529
+ top left, values from 0 to 1. Convert from preview coordinates with
530
+ `metadataOutputRectConverted(fromLayerRect:)` rather than guessing.