@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0

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