@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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,774 +1,530 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Capture session recipes
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
10
|
-
- [2.
|
|
11
|
-
- [3.
|
|
12
|
-
- [4.
|
|
13
|
-
- [5. Flash and
|
|
14
|
-
- [6.
|
|
15
|
-
- [7.
|
|
16
|
-
- [8.
|
|
17
|
-
- [9.
|
|
18
|
-
- [10.
|
|
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.
|
|
21
|
+
## 1. A full photo camera
|
|
21
22
|
|
|
22
|
-
|
|
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
|
|
29
|
+
import Observation
|
|
27
30
|
|
|
28
|
-
@
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
95
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
156
|
-
nonisolated func photoOutput(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
132
|
+
latestShot = encoded
|
|
133
|
+
pendingShot?.resume(returning: encoded)
|
|
134
|
+
pendingShot = nil
|
|
166
135
|
}
|
|
167
136
|
}
|
|
168
137
|
}
|
|
169
138
|
```
|
|
170
139
|
|
|
171
|
-
|
|
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.
|
|
156
|
+
## 2. Preview view
|
|
176
157
|
|
|
177
158
|
```swift
|
|
178
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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.
|
|
202
|
+
## 3. Camera screen
|
|
214
203
|
|
|
215
204
|
```swift
|
|
216
|
-
import SwiftUI
|
|
217
|
-
|
|
218
205
|
@available(iOS 17.0, *)
|
|
219
|
-
struct
|
|
220
|
-
@State private var camera =
|
|
221
|
-
@State private var
|
|
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
|
-
|
|
226
|
-
.ignoresSafeArea()
|
|
227
|
-
|
|
212
|
+
CameraSurface(session: camera.rig.session).ignoresSafeArea()
|
|
228
213
|
VStack {
|
|
229
|
-
// Top controls
|
|
230
214
|
HStack {
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
session.
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
391
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
419
|
-
nonisolated func fileOutput(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
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
|
-
|
|
428
|
-
|
|
429
|
-
videoContinuation = nil
|
|
344
|
+
finishing?.resume(returning: finished)
|
|
345
|
+
finishing = nil
|
|
430
346
|
}
|
|
431
347
|
}
|
|
432
348
|
}
|
|
433
349
|
```
|
|
434
350
|
|
|
435
|
-
|
|
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
|
|
356
|
+
## 5. Flash and torch
|
|
440
357
|
|
|
441
|
-
Flash
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
if
|
|
452
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
---
|
|
375
|
+
Wrap every device change in `lockForConfiguration()` and
|
|
376
|
+
`unlockForConfiguration()`; other clients can be configuring the same device.
|
|
461
377
|
|
|
462
|
-
## 6.
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
private let
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
424
|
+
func start() async {
|
|
425
|
+
payload = nil
|
|
580
426
|
isScanning = true
|
|
581
|
-
|
|
427
|
+
await rig.onQueue { [rig] in rig.session.startRunning() }
|
|
582
428
|
}
|
|
583
429
|
|
|
584
|
-
func
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
+
## 8. Orientation
|
|
662
454
|
|
|
663
|
-
|
|
455
|
+
On iOS 17 and later, rotation is an angle on the connection. `videoOrientation`
|
|
456
|
+
is deprecated.
|
|
664
457
|
|
|
665
458
|
```swift
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
|
753
|
-
|
|
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.
|
|
515
|
+
## 10. Limiting the scan area
|
|
766
516
|
|
|
767
|
-
|
|
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
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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.
|