@devo-bmad-custom/agent-orchestration 1.0.2 → 1.0.3
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/lib/installer.js +33 -0
- package/package.json +1 -1
- package/src/.agents/skills/audit-website/README.md +20 -20
- package/src/.agents/skills/audit-website/SKILL.md +470 -470
- package/src/.agents/skills/audit-website/agents/openai.yaml +6 -6
- package/src/.agents/skills/audit-website/assets/icon-small.svg +41 -41
- package/src/.agents/skills/audit-website/references/OUTPUT-FORMAT.md +250 -250
- package/src/.agents/skills/clean-code-standards/SKILL.md +104 -104
- package/src/.agents/skills/excalidraw-dark-standard/SKILL.md +281 -281
- package/src/.agents/skills/frontend-responsive-design-standards/SKILL.md +434 -434
- package/src/.agents/skills/java-fundamentals/SKILL.md +116 -116
- package/src/.agents/skills/java-performance/SKILL.md +119 -119
- package/src/.agents/skills/next-best-practices/SKILL.md +153 -153
- package/src/.agents/skills/next-best-practices/async-patterns.md +87 -87
- package/src/.agents/skills/next-best-practices/bundling.md +180 -180
- package/src/.agents/skills/next-best-practices/data-patterns.md +297 -297
- package/src/.agents/skills/next-best-practices/debug-tricks.md +105 -105
- package/src/.agents/skills/next-best-practices/directives.md +73 -73
- package/src/.agents/skills/next-best-practices/error-handling.md +227 -227
- package/src/.agents/skills/next-best-practices/file-conventions.md +140 -140
- package/src/.agents/skills/next-best-practices/font.md +245 -245
- package/src/.agents/skills/next-best-practices/functions.md +108 -108
- package/src/.agents/skills/next-best-practices/hydration-error.md +91 -91
- package/src/.agents/skills/next-best-practices/image.md +173 -173
- package/src/.agents/skills/next-best-practices/metadata.md +301 -301
- package/src/.agents/skills/next-best-practices/parallel-routes.md +287 -287
- package/src/.agents/skills/next-best-practices/route-handlers.md +146 -146
- package/src/.agents/skills/next-best-practices/rsc-boundaries.md +159 -159
- package/src/.agents/skills/next-best-practices/runtime-selection.md +39 -39
- package/src/.agents/skills/next-best-practices/scripts.md +141 -141
- package/src/.agents/skills/next-best-practices/self-hosting.md +371 -371
- package/src/.agents/skills/next-best-practices/suspense-boundaries.md +67 -67
- package/src/.agents/skills/nextjs-app-router-patterns/SKILL.md +537 -537
- package/src/.agents/skills/postgresql-optimization/SKILL.md +404 -404
- package/src/.agents/skills/python-backend/SKILL.md +153 -153
- package/src/.agents/skills/python-fundamentals/SKILL.md +234 -234
- package/src/.agents/skills/python-performance/SKILL.md +404 -404
- package/src/.agents/skills/react-expert/SKILL.md +335 -335
- package/src/.agents/skills/redis-best-practices/SKILL.md +438 -438
- package/src/.agents/skills/security-best-practices/SKILL.md +288 -288
- package/src/.agents/skills/security-review/LICENSE +22 -22
- package/src/.agents/skills/security-review/SKILL.md +312 -312
- package/src/.agents/skills/security-review/infrastructure/docker.md +432 -432
- package/src/.agents/skills/security-review/languages/javascript.md +388 -388
- package/src/.agents/skills/security-review/languages/python.md +363 -363
- package/src/.agents/skills/security-review/references/api-security.md +519 -519
- package/src/.agents/skills/security-review/references/authentication.md +353 -353
- package/src/.agents/skills/security-review/references/authorization.md +372 -372
- package/src/.agents/skills/security-review/references/business-logic.md +443 -443
- package/src/.agents/skills/security-review/references/cryptography.md +329 -329
- package/src/.agents/skills/security-review/references/csrf.md +398 -398
- package/src/.agents/skills/security-review/references/data-protection.md +378 -378
- package/src/.agents/skills/security-review/references/deserialization.md +410 -410
- package/src/.agents/skills/security-review/references/error-handling.md +436 -436
- package/src/.agents/skills/security-review/references/file-security.md +457 -457
- package/src/.agents/skills/security-review/references/injection.md +259 -259
- package/src/.agents/skills/security-review/references/logging.md +433 -433
- package/src/.agents/skills/security-review/references/misconfiguration.md +435 -435
- package/src/.agents/skills/security-review/references/modern-threats.md +475 -475
- package/src/.agents/skills/security-review/references/ssrf.md +415 -415
- package/src/.agents/skills/security-review/references/supply-chain.md +405 -405
- package/src/.agents/skills/security-review/references/xss.md +336 -336
- package/src/.agents/skills/subagent-driven-development/SKILL.md +275 -275
- package/src/.agents/skills/subagent-driven-development/code-quality-reviewer-prompt.md +26 -26
- package/src/.agents/skills/subagent-driven-development/implementer-prompt.md +113 -113
- package/src/.agents/skills/subagent-driven-development/spec-reviewer-prompt.md +61 -61
- package/src/.agents/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/src/.agents/skills/systematic-debugging/SKILL.md +296 -296
- package/src/.agents/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/src/.agents/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/src/.agents/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/src/.agents/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/src/.agents/skills/systematic-debugging/test-academic.md +14 -14
- package/src/.agents/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/src/.agents/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/src/.agents/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/src/.agents/skills/typescript-best-practices/SKILL.md +373 -373
- package/src/.agents/skills/ui-ux-pro-custom/SKILL.md +348 -348
- package/src/.agents/skills/ui-ux-pro-custom/data/charts.csv +26 -26
- package/src/.agents/skills/ui-ux-pro-custom/data/colors.csv +97 -97
- package/src/.agents/skills/ui-ux-pro-custom/data/icons.csv +101 -101
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/SKILL.md +106 -106
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/accessibility.md +475 -475
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/animation.md +466 -466
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/composition-locals.md +231 -231
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/deprecated-patterns.md +323 -323
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/lists-scrolling.md +400 -400
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/modifiers.md +331 -331
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/navigation.md +416 -416
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/performance.md +446 -446
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/side-effects.md +516 -516
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/foundation-source.md +13327 -13327
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/material3-source.md +19097 -19097
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/navigation-source.md +2947 -2947
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/runtime-source.md +11316 -11316
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/ui-source.md +7896 -7896
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/state-management.md +377 -377
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/styles-experimental.md +470 -470
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/theming-material3.md +349 -349
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/view-composition.md +595 -595
- package/src/.agents/skills/ui-ux-pro-custom/data/landing.csv +31 -31
- package/src/.agents/skills/ui-ux-pro-custom/data/mobile-ui-layout.md +654 -654
- package/src/.agents/skills/ui-ux-pro-custom/data/products.csv +96 -96
- package/src/.agents/skills/ui-ux-pro-custom/data/react-performance.csv +45 -45
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/astro.csv +54 -54
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/flutter.csv +53 -53
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/html-tailwind.csv +56 -56
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/jetpack-compose.csv +53 -53
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/nextjs.csv +53 -53
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/nuxt-ui.csv +51 -51
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/nuxtjs.csv +59 -59
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/react-native.csv +56 -56
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/react.csv +54 -54
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/shadcn.csv +61 -61
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/svelte.csv +54 -54
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/swiftui.csv +51 -51
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/vue.csv +50 -50
- package/src/.agents/skills/ui-ux-pro-custom/data/styles.csv +68 -68
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/alarmkit/SKILL.md +438 -438
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/alarmkit/references/alarmkit-patterns.md +584 -584
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-clips/SKILL.md +436 -436
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-intents/SKILL.md +489 -489
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-intents/references/appintents-advanced.md +1076 -1076
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-store-review/SKILL.md +340 -340
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-store-review/references/privacy-manifest.md +90 -90
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-store-review/references/review-checklists.md +106 -106
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/SKILL.md +500 -500
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/references/coreml-conversion.md +425 -425
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/references/coreml-optimization.md +344 -344
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/references/foundation-models.md +508 -508
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/references/mlx-swift.md +285 -285
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/authentication/SKILL.md +496 -496
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/authentication/references/keychain-biometric.md +211 -211
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/background-processing/SKILL.md +499 -499
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/background-processing/references/background-task-patterns.md +390 -390
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/callkit-voip/SKILL.md +461 -461
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/callkit-voip/references/callkit-patterns.md +425 -425
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/cloudkit-sync/SKILL.md +492 -492
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/cloudkit-sync/references/cloudkit-patterns.md +461 -461
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/codable-patterns/SKILL.md +467 -467
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/contacts-framework/SKILL.md +425 -425
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/contacts-framework/references/contacts-patterns.md +409 -409
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-bluetooth/SKILL.md +491 -491
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-bluetooth/references/ble-patterns.md +435 -435
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-motion/SKILL.md +388 -388
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-motion/references/motion-patterns.md +405 -405
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-nfc/SKILL.md +495 -495
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-nfc/references/nfc-patterns.md +420 -420
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/coreml/SKILL.md +459 -459
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/coreml/references/coreml-swift-integration.md +765 -765
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/debugging-instruments/SKILL.md +422 -422
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/debugging-instruments/references/instruments-guide.md +387 -387
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/debugging-instruments/references/lldb-patterns.md +298 -298
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/device-integrity/SKILL.md +477 -477
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/energykit/SKILL.md +460 -460
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/energykit/references/energykit-patterns.md +541 -541
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/eventkit-calendar/SKILL.md +483 -483
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/eventkit-calendar/references/eventkit-patterns.md +326 -326
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/healthkit/SKILL.md +498 -498
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/healthkit/references/healthkit-patterns.md +602 -602
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/homekit-matter/SKILL.md +496 -496
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/homekit-matter/references/matter-commissioning.md +455 -455
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-accessibility/SKILL.md +301 -301
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-accessibility/references/a11y-patterns.md +140 -140
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-localization/SKILL.md +418 -418
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-localization/references/formatstyle-locale.md +627 -627
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-localization/references/string-catalogs.md +462 -462
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/SKILL.md +441 -441
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/references/background-websocket.md +862 -862
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/references/lightweight-clients.md +93 -93
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/references/network-framework.md +563 -563
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/references/urlsession-patterns.md +1116 -1116
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/SKILL.md +496 -496
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/references/app-review-guidelines.md +174 -174
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/references/cryptokit-advanced.md +296 -296
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/references/file-storage-patterns.md +354 -354
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/references/privacy-manifest.md +117 -117
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/live-activities/SKILL.md +500 -500
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/live-activities/references/live-activity-patterns.md +868 -868
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/mapkit-location/SKILL.md +485 -485
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/mapkit-location/references/corelocation-patterns.md +730 -730
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/mapkit-location/references/mapkit-patterns.md +748 -748
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/metrickit-diagnostics/SKILL.md +479 -479
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/musickit-audio/SKILL.md +395 -395
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/musickit-audio/references/musickit-patterns.md +363 -363
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/natural-language/SKILL.md +412 -412
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/natural-language/references/translation-patterns.md +311 -311
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/passkit-wallet/SKILL.md +398 -398
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/passkit-wallet/references/wallet-passes.md +254 -254
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/pencilkit-drawing/SKILL.md +387 -387
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/pencilkit-drawing/references/paperkit-integration.md +376 -376
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/pencilkit-drawing/references/pencilkit-patterns.md +302 -302
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/permissionkit/SKILL.md +446 -446
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/permissionkit/references/permissionkit-patterns.md +435 -435
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/SKILL.md +500 -500
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/references/av-playback.md +701 -701
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/references/camera-capture.md +774 -774
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/references/image-loading-caching.md +869 -869
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/references/photospicker-patterns.md +597 -597
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/push-notifications/SKILL.md +500 -500
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/push-notifications/references/notification-patterns.md +677 -677
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/push-notifications/references/rich-notifications.md +745 -745
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/realitykit-ar/SKILL.md +479 -479
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/realitykit-ar/references/realitykit-patterns.md +480 -480
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/shareplay-activities/SKILL.md +483 -483
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/shareplay-activities/references/shareplay-patterns.md +544 -544
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/speech-recognition/SKILL.md +485 -485
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/storekit/SKILL.md +478 -478
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/storekit/references/app-review-guidelines.md +58 -58
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/storekit/references/storekit-advanced.md +755 -755
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-charts/SKILL.md +487 -487
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-charts/references/charts-patterns.md +895 -895
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/SKILL.md +408 -408
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/references/approachable-concurrency.md +80 -80
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/references/swift-6-2-concurrency.md +233 -233
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/references/swiftui-concurrency.md +187 -187
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/references/synchronization-primitives.md +341 -341
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-language/SKILL.md +498 -498
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-language/references/swift-patterns-extended.md +505 -505
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-testing/SKILL.md +467 -467
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-testing/references/testing-patterns.md +504 -504
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftdata/SKILL.md +334 -334
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftdata/references/core-data-coexistence.md +504 -504
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftdata/references/swiftdata-advanced.md +975 -975
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftdata/references/swiftdata-queries.md +675 -675
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-animation/SKILL.md +481 -481
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-animation/references/animation-advanced.md +804 -804
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-animation/references/core-animation-bridge.md +553 -553
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-gestures/SKILL.md +450 -450
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-gestures/references/gesture-patterns.md +425 -425
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/SKILL.md +336 -336
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/references/form.md +97 -97
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/references/grids.md +69 -69
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/references/list.md +99 -99
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/references/scrollview.md +147 -147
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-liquid-glass/SKILL.md +325 -325
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-liquid-glass/references/liquid-glass.md +387 -387
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/SKILL.md +262 -262
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/references/deeplinks.md +207 -207
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/references/navigationstack.md +177 -177
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/references/sheets.md +169 -169
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/references/tabview.md +178 -178
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/SKILL.md +381 -381
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/references/architecture-patterns.md +486 -486
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/references/deprecated-migration.md +1097 -1097
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/references/design-polish.md +780 -780
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/references/platform-and-sharing.md +696 -696
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/SKILL.md +491 -491
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +46 -46
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +29 -29
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/references/understanding-hangs-in-your-app.md +33 -33
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/references/understanding-improving-swiftui-performance.md +52 -52
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-uikit-interop/SKILL.md +428 -428
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-uikit-interop/references/hosting-migration.md +534 -534
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-uikit-interop/references/representable-recipes.md +1133 -1133
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/tipkit/SKILL.md +494 -494
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/tipkit/references/tipkit-patterns.md +782 -782
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/SKILL.md +475 -475
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/vision-requests.md +736 -736
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/visionkit-scanner.md +738 -738
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/SKILL.md +410 -410
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/references/weatherkit-patterns.md +567 -567
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/SKILL.md +497 -497
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/references/widgetkit-advanced.md +871 -871
- package/src/.agents/skills/ui-ux-pro-custom/data/typography.csv +57 -57
- package/src/.agents/skills/ui-ux-pro-custom/data/ui-reasoning.csv +101 -101
- package/src/.agents/skills/ui-ux-pro-custom/data/ux-guidelines.csv +99 -99
- package/src/.agents/skills/ui-ux-pro-custom/data/web-interface.csv +31 -31
- package/src/.agents/skills/ui-ux-pro-custom/scripts/core.py +253 -253
- package/src/.agents/skills/ui-ux-pro-custom/scripts/design_system.py +1067 -1067
- package/src/.agents/skills/ui-ux-pro-custom/scripts/search.py +114 -114
- package/src/.agents/skills/ux-audit/SKILL.md +150 -150
- package/src/.agents/skills/websocket-engineer/SKILL.md +168 -168
- package/src/.agents/skills/websocket-engineer/references/alternatives.md +391 -391
- package/src/.agents/skills/websocket-engineer/references/patterns.md +400 -400
- package/src/.agents/skills/websocket-engineer/references/protocol.md +195 -195
- package/src/.agents/skills/websocket-engineer/references/scaling.md +333 -333
- package/src/.agents/skills/websocket-engineer/references/security.md +474 -474
- package/src/.agents/skills/writing-skills/SKILL.md +655 -655
- package/src/.agents/skills/writing-skills/anthropic-best-practices.md +1150 -1150
- package/src/.agents/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -189
- package/src/.agents/skills/writing-skills/graphviz-conventions.dot +171 -171
- package/src/.agents/skills/writing-skills/persuasion-principles.md +187 -187
- package/src/.agents/skills/writing-skills/render-graphs.js +168 -168
- package/src/.agents/skills/writing-skills/testing-skills-with-subagents.md +384 -384
- package/src/.claude/commands/master-orchestrator.md +15 -0
- package/src/_memory/config.yaml +11 -11
- package/src/_memory/master-orchestrator-sidecar/instructions.md +85 -32
- package/src/_memory/skills/nimbalyst-tracking/SKILL.md +103 -103
- package/src/_memory/skills/writing-skills/SKILL.md +655 -655
- package/src/bmb/agents/agent-builder.md +59 -59
- package/src/bmb/agents/module-builder.md +60 -60
- package/src/bmb/agents/workflow-builder.md +61 -61
- package/src/bmb/config.yaml +12 -12
- package/src/bmb/module-help.csv +13 -13
- package/src/bmb/workflows/agent/data/agent-architecture.md +258 -258
- package/src/bmb/workflows/agent/data/agent-compilation.md +185 -185
- package/src/bmb/workflows/agent/data/agent-menu-patterns.md +189 -189
- package/src/bmb/workflows/agent/data/agent-metadata.md +133 -133
- package/src/bmb/workflows/agent/data/agent-validation.md +111 -111
- package/src/bmb/workflows/agent/data/brainstorm-context.md +96 -96
- package/src/bmb/workflows/agent/data/communication-presets.csv +61 -61
- package/src/bmb/workflows/agent/data/critical-actions.md +75 -75
- package/src/bmb/workflows/agent/data/persona-properties.md +252 -252
- package/src/bmb/workflows/agent/data/principles-crafting.md +142 -142
- package/src/bmb/workflows/agent/data/reference/module-examples/architect.md +68 -68
- package/src/bmb/workflows/agent/data/reference/with-sidecar/journal-keeper/journal-keeper-sidecar/entries/yy-mm-dd-entry-template.md +16 -16
- package/src/bmb/workflows/agent/data/understanding-agent-types.md +126 -126
- package/src/bmb/workflows/agent/steps-c/step-01-brainstorm.md +129 -129
- package/src/bmb/workflows/agent/steps-c/step-02-discovery.md +170 -170
- package/src/bmb/workflows/agent/steps-c/step-03-sidecar-metadata.md +309 -309
- package/src/bmb/workflows/agent/steps-c/step-04-persona.md +213 -213
- package/src/bmb/workflows/agent/steps-c/step-05-commands-menu.md +179 -179
- package/src/bmb/workflows/agent/steps-c/step-06-activation.md +278 -278
- package/src/bmb/workflows/agent/steps-c/step-07-build-agent.md +316 -316
- package/src/bmb/workflows/agent/steps-c/step-08-celebrate.md +247 -247
- package/src/bmb/workflows/agent/steps-e/e-01-load-existing.md +221 -221
- package/src/bmb/workflows/agent/steps-e/e-02-discover-edits.md +195 -195
- package/src/bmb/workflows/agent/steps-e/e-04-sidecar-metadata.md +126 -126
- package/src/bmb/workflows/agent/steps-e/e-05-persona.md +135 -135
- package/src/bmb/workflows/agent/steps-e/e-06-commands-menu.md +123 -123
- package/src/bmb/workflows/agent/steps-e/e-07-activation.md +124 -124
- package/src/bmb/workflows/agent/steps-e/e-08-edit-agent.md +197 -197
- package/src/bmb/workflows/agent/steps-e/e-09-celebrate.md +155 -155
- package/src/bmb/workflows/agent/steps-v/v-01-load-review.md +137 -137
- package/src/bmb/workflows/agent/steps-v/v-02a-validate-metadata.md +116 -116
- package/src/bmb/workflows/agent/steps-v/v-02b-validate-persona.md +124 -124
- package/src/bmb/workflows/agent/steps-v/v-02c-validate-menu.md +127 -127
- package/src/bmb/workflows/agent/steps-v/v-02d-validate-structure.md +134 -134
- package/src/bmb/workflows/agent/steps-v/v-02e-validate-sidecar.md +134 -134
- package/src/bmb/workflows/agent/steps-v/v-03-summary.md +104 -104
- package/src/bmb/workflows/agent/templates/agent-plan.template.md +5 -5
- package/src/bmb/workflows/agent/templates/agent-template.md +89 -89
- package/src/bmb/workflows/agent/workflow-create-agent.md +72 -72
- package/src/bmb/workflows/agent/workflow-edit-agent.md +75 -75
- package/src/bmb/workflows/agent/workflow-validate-agent.md +73 -73
- package/src/bmb/workflows/module/data/agent-architecture.md +179 -179
- package/src/bmb/workflows/module/data/agent-spec-template.md +79 -79
- package/src/bmb/workflows/module/data/module-standards.md +263 -263
- package/src/bmb/workflows/module/data/module-yaml-conventions.md +392 -392
- package/src/bmb/workflows/module/module-help-generate.md +254 -254
- package/src/bmb/workflows/module/steps-b/step-01-welcome.md +148 -148
- package/src/bmb/workflows/module/steps-b/step-02-spark.md +141 -141
- package/src/bmb/workflows/module/steps-b/step-03-module-type.md +149 -149
- package/src/bmb/workflows/module/steps-b/step-04-vision.md +83 -83
- package/src/bmb/workflows/module/steps-b/step-05-identity.md +97 -97
- package/src/bmb/workflows/module/steps-b/step-06-users.md +86 -86
- package/src/bmb/workflows/module/steps-b/step-07-value.md +76 -76
- package/src/bmb/workflows/module/steps-b/step-08-agents.md +97 -97
- package/src/bmb/workflows/module/steps-b/step-09-workflows.md +83 -83
- package/src/bmb/workflows/module/steps-b/step-10-tools.md +91 -91
- package/src/bmb/workflows/module/steps-b/step-11-scenarios.md +84 -84
- package/src/bmb/workflows/module/steps-b/step-12-creative.md +95 -95
- package/src/bmb/workflows/module/steps-b/step-13-review.md +105 -105
- package/src/bmb/workflows/module/steps-b/step-14-finalize.md +117 -117
- package/src/bmb/workflows/module/steps-c/step-01-load-brief.md +179 -179
- package/src/bmb/workflows/module/steps-c/step-01b-continue.md +82 -82
- package/src/bmb/workflows/module/steps-c/step-02-structure.md +105 -105
- package/src/bmb/workflows/module/steps-c/step-03-config.md +119 -119
- package/src/bmb/workflows/module/steps-c/step-04-agents.md +168 -168
- package/src/bmb/workflows/module/steps-c/step-05-workflows.md +184 -184
- package/src/bmb/workflows/module/steps-c/step-06-docs.md +401 -401
- package/src/bmb/workflows/module/steps-c/step-07-complete.md +152 -152
- package/src/bmb/workflows/module/steps-e/step-01-load-target.md +81 -81
- package/src/bmb/workflows/module/steps-e/step-02-select-edit.md +77 -77
- package/src/bmb/workflows/module/steps-e/step-03-apply-edit.md +77 -77
- package/src/bmb/workflows/module/steps-e/step-04-review.md +80 -80
- package/src/bmb/workflows/module/steps-e/step-05-confirm.md +75 -75
- package/src/bmb/workflows/module/steps-v/step-01-load-target.md +96 -96
- package/src/bmb/workflows/module/steps-v/step-02-file-structure.md +93 -93
- package/src/bmb/workflows/module/steps-v/step-03-module-yaml.md +99 -99
- package/src/bmb/workflows/module/steps-v/step-04-agent-specs.md +152 -152
- package/src/bmb/workflows/module/steps-v/step-05-workflow-specs.md +152 -152
- package/src/bmb/workflows/module/steps-v/step-06-documentation.md +143 -143
- package/src/bmb/workflows/module/steps-v/step-07-installation.md +102 -102
- package/src/bmb/workflows/module/steps-v/step-08-report.md +197 -197
- package/src/bmb/workflows/module/templates/brief-template.md +154 -154
- package/src/bmb/workflows/module/templates/workflow-spec-template.md +96 -96
- package/src/bmb/workflows/module/workflow-create-module-brief.md +71 -71
- package/src/bmb/workflows/module/workflow-create-module.md +86 -86
- package/src/bmb/workflows/module/workflow-edit-module.md +66 -66
- package/src/bmb/workflows/module/workflow-validate-module.md +66 -66
- package/src/bmb/workflows/workflow/data/architecture.md +150 -150
- package/src/bmb/workflows/workflow/data/common-workflow-tools.csv +19 -19
- package/src/bmb/workflows/workflow/data/csv-data-file-standards.md +53 -53
- package/src/bmb/workflows/workflow/data/frontmatter-standards.md +184 -184
- package/src/bmb/workflows/workflow/data/input-discovery-standards.md +191 -191
- package/src/bmb/workflows/workflow/data/intent-vs-prescriptive-spectrum.md +44 -44
- package/src/bmb/workflows/workflow/data/menu-handling-standards.md +133 -133
- package/src/bmb/workflows/workflow/data/output-format-standards.md +135 -135
- package/src/bmb/workflows/workflow/data/step-file-rules.md +235 -235
- package/src/bmb/workflows/workflow/data/step-type-patterns.md +257 -257
- package/src/bmb/workflows/workflow/data/subprocess-optimization-patterns.md +188 -188
- package/src/bmb/workflows/workflow/data/trimodal-workflow-structure.md +164 -164
- package/src/bmb/workflows/workflow/data/workflow-chaining-standards.md +222 -222
- package/src/bmb/workflows/workflow/data/workflow-examples.md +232 -232
- package/src/bmb/workflows/workflow/data/workflow-type-criteria.md +134 -134
- package/src/bmb/workflows/workflow/steps-c/step-00-conversion.md +263 -263
- package/src/bmb/workflows/workflow/steps-c/step-01-discovery.md +194 -194
- package/src/bmb/workflows/workflow/steps-c/step-01b-continuation.md +3 -3
- package/src/bmb/workflows/workflow/steps-c/step-02-classification.md +270 -270
- package/src/bmb/workflows/workflow/steps-c/step-03-requirements.md +283 -283
- package/src/bmb/workflows/workflow/steps-c/step-04-tools.md +282 -282
- package/src/bmb/workflows/workflow/steps-c/step-05-plan-review.md +243 -243
- package/src/bmb/workflows/workflow/steps-c/step-06-design.md +330 -330
- package/src/bmb/workflows/workflow/steps-c/step-07-foundation.md +239 -239
- package/src/bmb/workflows/workflow/steps-c/step-08-build-step-01.md +379 -379
- package/src/bmb/workflows/workflow/steps-c/step-09-build-next-step.md +350 -350
- package/src/bmb/workflows/workflow/steps-c/step-10-confirmation.md +322 -322
- package/src/bmb/workflows/workflow/steps-c/step-11-completion.md +191 -191
- package/src/bmb/workflows/workflow/steps-e/step-e-01-assess-workflow.md +237 -237
- package/src/bmb/workflows/workflow/steps-e/step-e-02-discover-edits.md +251 -251
- package/src/bmb/workflows/workflow/steps-e/step-e-03-fix-validation.md +254 -254
- package/src/bmb/workflows/workflow/steps-e/step-e-04-direct-edit.md +277 -277
- package/src/bmb/workflows/workflow/steps-e/step-e-05-apply-edit.md +154 -154
- package/src/bmb/workflows/workflow/steps-e/step-e-06-validate-after.md +190 -190
- package/src/bmb/workflows/workflow/steps-e/step-e-07-complete.md +206 -206
- package/src/bmb/workflows/workflow/steps-v/step-01-validate-max-mode.md +109 -109
- package/src/bmb/workflows/workflow/steps-v/step-01-validate.md +221 -221
- package/src/bmb/workflows/workflow/steps-v/step-01b-structure.md +152 -152
- package/src/bmb/workflows/workflow/steps-v/step-02-frontmatter-validation.md +199 -199
- package/src/bmb/workflows/workflow/steps-v/step-02b-path-violations.md +265 -265
- package/src/bmb/workflows/workflow/steps-v/step-03-menu-validation.md +164 -164
- package/src/bmb/workflows/workflow/steps-v/step-04-step-type-validation.md +211 -211
- package/src/bmb/workflows/workflow/steps-v/step-05-output-format-validation.md +200 -200
- package/src/bmb/workflows/workflow/steps-v/step-06-validation-design-check.md +195 -195
- package/src/bmb/workflows/workflow/steps-v/step-07-instruction-style-check.md +209 -209
- package/src/bmb/workflows/workflow/steps-v/step-08-collaborative-experience-check.md +199 -199
- package/src/bmb/workflows/workflow/steps-v/step-08b-subprocess-optimization.md +179 -179
- package/src/bmb/workflows/workflow/steps-v/step-09-cohesive-review.md +186 -186
- package/src/bmb/workflows/workflow/steps-v/step-10-report-complete.md +154 -154
- package/src/bmb/workflows/workflow/steps-v/step-11-plan-validation.md +237 -237
- package/src/bmb/workflows/workflow/templates/minimal-output-template.md +11 -11
- package/src/bmb/workflows/workflow/templates/step-01-init-continuable-template.md +241 -241
- package/src/bmb/workflows/workflow/templates/step-1b-template.md +224 -224
- package/src/bmb/workflows/workflow/templates/step-template.md +294 -294
- package/src/bmb/workflows/workflow/templates/workflow-template.md +102 -102
- package/src/bmb/workflows/workflow/workflow-create-workflow.md +79 -79
- package/src/bmb/workflows/workflow/workflow-edit-workflow.md +65 -65
- package/src/bmb/workflows/workflow/workflow-rework-workflow.md +65 -65
- package/src/bmb/workflows/workflow/workflow-validate-max-parallel-workflow.md +66 -66
- package/src/bmb/workflows/workflow/workflow-validate-workflow.md +65 -65
- package/src/bmm/agents/analyst.md +104 -104
- package/src/bmm/agents/dev.md +100 -100
- package/src/bmm/agents/qa.md +100 -90
- package/src/bmm/agents/tech-writer/tech-writer.md +94 -94
- package/src/bmm/module-help.csv +31 -31
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-01-init.md +115 -115
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-01b-continue.md +107 -107
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-02-vision.md +141 -141
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-03-users.md +144 -144
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-04-metrics.md +147 -147
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-05-scope.md +161 -161
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md +99 -99
- package/src/bmm/workflows/1-analysis/create-product-brief/workflow.md +57 -57
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-01-init.md +87 -87
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-02-domain-analysis.md +156 -156
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-03-competitive-landscape.md +165 -165
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-04-regulatory-focus.md +140 -140
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-05-technical-trends.md +152 -152
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-06-research-synthesis.md +345 -345
- package/src/bmm/workflows/1-analysis/research/market-steps/step-01-init.md +92 -92
- package/src/bmm/workflows/1-analysis/research/market-steps/step-02-customer-behavior.md +164 -164
- package/src/bmm/workflows/1-analysis/research/market-steps/step-03-customer-pain-points.md +174 -174
- package/src/bmm/workflows/1-analysis/research/market-steps/step-04-customer-decisions.md +184 -184
- package/src/bmm/workflows/1-analysis/research/market-steps/step-05-competitive-analysis.md +105 -105
- package/src/bmm/workflows/1-analysis/research/market-steps/step-06-research-completion.md +360 -360
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-01-init.md +87 -87
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-02-technical-overview.md +165 -165
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-03-integration-patterns.md +174 -174
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md +141 -141
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md +159 -159
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md +387 -387
- package/src/bmm/workflows/1-analysis/research/workflow-domain-research.md +54 -54
- package/src/bmm/workflows/1-analysis/research/workflow-market-research.md +54 -54
- package/src/bmm/workflows/1-analysis/research/workflow-technical-research.md +54 -54
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-01b-continue.md +100 -100
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02-discovery.md +160 -160
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02b-vision.md +88 -88
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02c-executive-summary.md +99 -99
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-03-success.md +169 -169
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-04-journeys.md +156 -156
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-05-domain.md +136 -136
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-06-innovation.md +176 -176
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-07-project-type.md +184 -184
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-08-scoping.md +174 -174
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-09-functional.md +175 -175
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-10-nonfunctional.md +189 -189
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-11-polish.md +162 -162
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-12-complete.md +79 -79
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-01-discovery.md +183 -183
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-01b-legacy-conversion.md +149 -149
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-02-review.md +187 -187
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-03-edit.md +192 -192
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-04-complete.md +108 -108
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-01-discovery.md +166 -166
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-02-format-detection.md +131 -131
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-02b-parity-check.md +150 -150
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-03-density-validation.md +118 -118
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-04-brief-coverage-validation.md +155 -155
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-05-measurability-validation.md +170 -170
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-06-traceability-validation.md +158 -158
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-07-implementation-leakage-validation.md +147 -147
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-08-domain-compliance-validation.md +182 -182
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-09-project-type-validation.md +202 -202
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-10-smart-validation.md +148 -148
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-11-holistic-quality-validation.md +201 -201
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-12-completeness-validation.md +179 -179
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md +164 -164
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md +65 -65
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md +65 -65
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md +63 -63
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-01b-continue.md +63 -63
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-02-discovery.md +106 -106
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-03-core-experience.md +111 -111
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-04-emotional-response.md +115 -115
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-05-inspiration.md +127 -127
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-06-design-system.md +167 -167
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-07-defining-experience.md +143 -143
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-08-visual-foundation.md +118 -118
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-09-design-directions.md +154 -154
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-10-user-journeys.md +136 -136
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-11-component-strategy.md +165 -165
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-12-ux-patterns.md +135 -135
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-13-responsive-accessibility.md +192 -192
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md +101 -101
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +45 -45
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md +185 -185
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md +129 -129
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md +130 -130
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md +93 -93
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md +196 -196
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md +129 -129
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +54 -54
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-01b-continue.md +82 -82
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-02-context.md +106 -106
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-03-starter.md +138 -138
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-04-decisions.md +129 -129
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-05-patterns.md +166 -166
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-06-structure.md +186 -186
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-07-validation.md +163 -163
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md +38 -38
- package/src/bmm/workflows/3-solutioning/create-architecture/workflow.md +49 -49
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-02-design-epics.md +124 -124
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-03-create-stories.md +122 -122
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md +84 -84
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +58 -58
- package/src/bmm/workflows/4-implementation/code-review/workflow.yaml +43 -43
- package/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +53 -53
- package/src/bmm/workflows/4-implementation/create-story/checklist.md +159 -159
- package/src/bmm/workflows/4-implementation/create-story/template.md +79 -79
- package/src/bmm/workflows/4-implementation/create-story/workflow.yaml +52 -52
- package/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +20 -20
- package/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +52 -52
- package/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +52 -52
- package/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +25 -25
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md +158 -158
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md +122 -122
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md +93 -93
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md +93 -93
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md +87 -87
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md +146 -146
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md +50 -50
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md +152 -152
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md +123 -123
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md +201 -201
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +79 -79
- package/src/bmm/workflows/document-project/workflow.yaml +22 -22
- package/src/bmm/workflows/generate-project-context/steps/step-01-discover.md +184 -184
- package/src/bmm/workflows/generate-project-context/steps/step-02-generate.md +322 -322
- package/src/bmm/workflows/generate-project-context/steps/step-03-complete.md +235 -235
- package/src/bmm/workflows/generate-project-context/workflow.md +49 -49
- package/src/bmm/workflows/qa/automate/workflow.yaml +233 -233
- package/src/bmm/workflows/qa-generate-e2e-tests/workflow.yaml +42 -42
- package/src/core/config.yaml +9 -9
- package/src/core/module-help.csv +10 -10
- package/src/core/scripts/generate-loop-report.py +72 -72
- package/src/core/tasks/editorial-review-prose.xml +101 -101
- package/src/core/tasks/editorial-review-structure.xml +207 -207
- package/src/core/tasks/help.md +86 -86
- package/src/core/tasks/index-docs.xml +64 -64
- package/src/core/tasks/review-adversarial-general.xml +66 -66
- package/src/core/tasks/review-adversarial-loop.xml +46 -46
- package/src/core/tasks/review-edge-case-hunter.xml +63 -63
- package/src/core/tasks/review-party-loop.xml +46 -46
- package/src/core/tasks/shard-doc.xml +107 -107
- package/src/core/tasks/workflow.xml +235 -235
- package/src/core/templates/review-loop-report.html +88 -88
- package/src/core/templates/review-loop-report.md +5 -5
- package/src/core/workflows/advanced-elicitation/workflow.xml +117 -117
- package/src/core/workflows/brainstorming/steps/step-01-session-setup.md +212 -212
- package/src/core/workflows/brainstorming/steps/step-01b-continue.md +122 -122
- package/src/core/workflows/brainstorming/steps/step-02a-user-selected.md +225 -225
- package/src/core/workflows/brainstorming/steps/step-02b-ai-recommended.md +237 -237
- package/src/core/workflows/brainstorming/steps/step-02c-random-selection.md +209 -209
- package/src/core/workflows/brainstorming/steps/step-02d-progressive-flow.md +264 -264
- package/src/core/workflows/brainstorming/steps/step-02e-deep-dive.md +68 -68
- package/src/core/workflows/brainstorming/steps/step-03-technique-execution.md +403 -403
- package/src/core/workflows/brainstorming/steps/step-04-idea-organization.md +303 -303
- package/src/core/workflows/brainstorming/workflow.md +60 -60
- package/src/core/workflows/extract-trackers/workflow.md +45 -45
- package/src/core/workflows/party-mode/steps/step-01-agent-loading.md +142 -142
- package/src/core/workflows/party-mode/workflow.md +194 -194
- package/src/docs/dev/tmux/actions_popup.py +291 -291
- package/src/docs/dev/tmux/tmux-setup.md +62 -1
|
@@ -1,774 +1,774 @@
|
|
|
1
|
-
# Camera Capture
|
|
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
|
-
---
|
|
6
|
-
|
|
7
|
-
## Contents
|
|
8
|
-
|
|
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)
|
|
19
|
-
|
|
20
|
-
## 1. Complete Camera Manager with Photo Capture
|
|
21
|
-
|
|
22
|
-
A full-featured camera model with photo capture using the delegate pattern.
|
|
23
|
-
|
|
24
|
-
```swift
|
|
25
|
-
import AVFoundation
|
|
26
|
-
import UIKit
|
|
27
|
-
|
|
28
|
-
@available(iOS 17.0, *)
|
|
29
|
-
@Observable
|
|
30
|
-
@MainActor
|
|
31
|
-
final class CameraManager: NSObject {
|
|
32
|
-
let session = AVCaptureSession()
|
|
33
|
-
|
|
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
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
session.beginConfiguration()
|
|
53
|
-
session.sessionPreset = .photo
|
|
54
|
-
|
|
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)
|
|
73
|
-
|
|
74
|
-
// Enable maximum quality (iOS 16+)
|
|
75
|
-
if let maxDimensions = photoOutput.maxPhotoDimensions(for: .photo) {
|
|
76
|
-
photoOutput.maxPhotoDimensions = maxDimensions
|
|
77
|
-
}
|
|
78
|
-
photoOutput.maxPhotoQualityPrioritization = .quality
|
|
79
|
-
|
|
80
|
-
session.commitConfiguration()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// MARK: - Session Control
|
|
84
|
-
|
|
85
|
-
func start() {
|
|
86
|
-
guard !session.isRunning else { return }
|
|
87
|
-
Task.detached { [session] in
|
|
88
|
-
session.startRunning()
|
|
89
|
-
}
|
|
90
|
-
isRunning = true
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
func stop() {
|
|
94
|
-
guard session.isRunning else { return }
|
|
95
|
-
Task.detached { [session] in
|
|
96
|
-
session.stopRunning()
|
|
97
|
-
}
|
|
98
|
-
isRunning = false
|
|
99
|
-
}
|
|
100
|
-
|
|
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
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
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)
|
|
147
|
-
}
|
|
148
|
-
return status == .authorized
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// MARK: - AVCapturePhotoCaptureDelegate
|
|
153
|
-
|
|
154
|
-
@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()
|
|
162
|
-
Task { @MainActor in
|
|
163
|
-
lastCapturedPhoto = data
|
|
164
|
-
photoContinuation?.resume(returning: data)
|
|
165
|
-
photoContinuation = nil
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
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.
|
|
172
|
-
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
## 2. Camera Preview (UIViewRepresentable)
|
|
176
|
-
|
|
177
|
-
```swift
|
|
178
|
-
import SwiftUI
|
|
179
|
-
import AVFoundation
|
|
180
|
-
|
|
181
|
-
struct CameraPreview: UIViewRepresentable {
|
|
182
|
-
let session: AVCaptureSession
|
|
183
|
-
|
|
184
|
-
func makeUIView(context: Context) -> CameraPreviewView {
|
|
185
|
-
let view = CameraPreviewView()
|
|
186
|
-
view.previewLayer.session = session
|
|
187
|
-
view.previewLayer.videoGravity = .resizeAspectFill
|
|
188
|
-
return view
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
func updateUIView(_ uiView: CameraPreviewView, context: Context) {
|
|
192
|
-
if uiView.previewLayer.session !== session {
|
|
193
|
-
uiView.previewLayer.session = session
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
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 }
|
|
202
|
-
|
|
203
|
-
var previewLayer: AVCaptureVideoPreviewLayer {
|
|
204
|
-
layer as! AVCaptureVideoPreviewLayer
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Never add `AVCaptureVideoPreviewLayer` as a sublayer manually. Using `layerClass` avoids manual frame management in `layoutSubviews`.
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## 3. Complete Camera Screen in SwiftUI
|
|
214
|
-
|
|
215
|
-
```swift
|
|
216
|
-
import SwiftUI
|
|
217
|
-
|
|
218
|
-
@available(iOS 17.0, *)
|
|
219
|
-
struct CameraScreen: View {
|
|
220
|
-
@State private var camera = CameraManager()
|
|
221
|
-
@State private var showCapturedPhoto = false
|
|
222
|
-
|
|
223
|
-
var body: some View {
|
|
224
|
-
ZStack {
|
|
225
|
-
CameraPreview(session: camera.session)
|
|
226
|
-
.ignoresSafeArea()
|
|
227
|
-
|
|
228
|
-
VStack {
|
|
229
|
-
// Top controls
|
|
230
|
-
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
|
-
|
|
240
|
-
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())
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
.padding(.horizontal)
|
|
254
|
-
|
|
255
|
-
Spacer()
|
|
256
|
-
|
|
257
|
-
// Bottom controls
|
|
258
|
-
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(RoundedRectangle(cornerRadius: 8))
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
Color.clear.frame(width: 50, height: 50)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
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
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
Spacer()
|
|
286
|
-
|
|
287
|
-
Color.clear.frame(width: 50, height: 50)
|
|
288
|
-
}
|
|
289
|
-
.padding(.horizontal, 24)
|
|
290
|
-
.padding(.bottom, 32)
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
.task {
|
|
294
|
-
await camera.configure()
|
|
295
|
-
camera.start()
|
|
296
|
-
}
|
|
297
|
-
.onDisappear {
|
|
298
|
-
camera.stop()
|
|
299
|
-
}
|
|
300
|
-
.sheet(isPresented: $showCapturedPhoto) {
|
|
301
|
-
if let data = camera.lastCapturedPhoto,
|
|
302
|
-
let uiImage = UIImage(data: data) {
|
|
303
|
-
Image(uiImage: uiImage)
|
|
304
|
-
.resizable()
|
|
305
|
-
.scaledToFit()
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
---
|
|
313
|
-
|
|
314
|
-
## 4. Video Recording
|
|
315
|
-
|
|
316
|
-
Add `AVCaptureMovieFileOutput` for video capture. Video recording requires `NSMicrophoneUsageDescription` in Info.plist for audio.
|
|
317
|
-
|
|
318
|
-
```swift
|
|
319
|
-
import AVFoundation
|
|
320
|
-
|
|
321
|
-
@available(iOS 17.0, *)
|
|
322
|
-
@Observable
|
|
323
|
-
@MainActor
|
|
324
|
-
final class VideoRecorder: NSObject {
|
|
325
|
-
let session = AVCaptureSession()
|
|
326
|
-
|
|
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
|
-
}
|
|
339
|
-
|
|
340
|
-
session.beginConfiguration()
|
|
341
|
-
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
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
func stopRecording() async -> URL? {
|
|
382
|
-
guard isRecording else { return nil }
|
|
383
|
-
|
|
384
|
-
return await withCheckedContinuation { continuation in
|
|
385
|
-
videoContinuation = continuation
|
|
386
|
-
movieOutput.stopRecording()
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
func start() {
|
|
391
|
-
Task.detached { [session] in session.startRunning() }
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
func stop() {
|
|
395
|
-
Task.detached { [session] in session.stopRunning() }
|
|
396
|
-
}
|
|
397
|
-
|
|
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)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return videoGranted
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
@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
|
-
) {
|
|
425
|
-
Task { @MainActor in
|
|
426
|
-
isRecording = false
|
|
427
|
-
recordedVideoURL = error == nil ? outputFileURL : nil
|
|
428
|
-
videoContinuation?.resume(returning: error == nil ? outputFileURL : nil)
|
|
429
|
-
videoContinuation = nil
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
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
|
-
---
|
|
438
|
-
|
|
439
|
-
## 5. Flash and Torch Control
|
|
440
|
-
|
|
441
|
-
Flash applies to photo capture settings. Torch provides continuous illumination for video or preview.
|
|
442
|
-
|
|
443
|
-
```swift
|
|
444
|
-
import AVFoundation
|
|
445
|
-
|
|
446
|
-
func toggleTorch(on device: AVCaptureDevice, enabled: Bool) throws {
|
|
447
|
-
guard device.hasTorch, device.isTorchAvailable else { return }
|
|
448
|
-
|
|
449
|
-
try device.lockForConfiguration()
|
|
450
|
-
device.torchMode = enabled ? .on : .off
|
|
451
|
-
if enabled {
|
|
452
|
-
try device.setTorchModeOn(level: AVCaptureDevice.maxAvailableTorchLevel)
|
|
453
|
-
}
|
|
454
|
-
device.unlockForConfiguration()
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
Always wrap device configuration in `lockForConfiguration()` / `unlockForConfiguration()`. Multiple clients may attempt to configure the device simultaneously.
|
|
459
|
-
|
|
460
|
-
---
|
|
461
|
-
|
|
462
|
-
## 6. Focus and Exposure
|
|
463
|
-
|
|
464
|
-
Implement tap-to-focus by converting a SwiftUI tap location to the camera coordinate system.
|
|
465
|
-
|
|
466
|
-
```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
|
-
|
|
476
|
-
try device.lockForConfiguration()
|
|
477
|
-
|
|
478
|
-
if device.isFocusPointOfInterestSupported {
|
|
479
|
-
device.focusPointOfInterest = devicePoint
|
|
480
|
-
device.focusMode = .autoFocus
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if device.isExposurePointOfInterestSupported {
|
|
484
|
-
device.exposurePointOfInterest = devicePoint
|
|
485
|
-
device.exposureMode = .autoExpose
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
device.unlockForConfiguration()
|
|
489
|
-
}
|
|
490
|
-
```
|
|
491
|
-
|
|
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
|
|
503
|
-
|
|
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.
|
|
536
|
-
|
|
537
|
-
```swift
|
|
538
|
-
import AVFoundation
|
|
539
|
-
|
|
540
|
-
@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
|
-
|
|
554
|
-
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
|
-
}
|
|
563
|
-
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()
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
func start() {
|
|
579
|
-
scannedCode = nil
|
|
580
|
-
isScanning = true
|
|
581
|
-
Task.detached { [session] in session.startRunning() }
|
|
582
|
-
}
|
|
583
|
-
|
|
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
|
-
|
|
608
|
-
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, 60)
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
.task {
|
|
645
|
-
await scanner.configure()
|
|
646
|
-
scanner.start()
|
|
647
|
-
}
|
|
648
|
-
.onDisappear {
|
|
649
|
-
scanner.stop()
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
```
|
|
654
|
-
|
|
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
|
-
---
|
|
658
|
-
|
|
659
|
-
## 8. Camera Preview Orientation
|
|
660
|
-
|
|
661
|
-
Handle device rotation so the preview and captured photos have correct orientation.
|
|
662
|
-
|
|
663
|
-
### Preview Layer Rotation
|
|
664
|
-
|
|
665
|
-
```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
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
let settings = AVCapturePhotoSettings()
|
|
709
|
-
photoOutput.capturePhoto(with: settings, delegate: self)
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
private func currentVideoRotationAngle() -> CGFloat {
|
|
713
|
-
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
|
|
714
|
-
return 90
|
|
715
|
-
}
|
|
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
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
```
|
|
725
|
-
|
|
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).
|
|
727
|
-
|
|
728
|
-
---
|
|
729
|
-
|
|
730
|
-
## 9. Dual Camera and Device Discovery
|
|
731
|
-
|
|
732
|
-
Select specific camera hardware using `AVCaptureDevice.DiscoverySession`.
|
|
733
|
-
|
|
734
|
-
```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
|
-
],
|
|
746
|
-
mediaType: .video,
|
|
747
|
-
position: .unspecified
|
|
748
|
-
)
|
|
749
|
-
return discoverySession.devices
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
func preferredBackCamera() -> AVCaptureDevice? {
|
|
753
|
-
// Prefer triple > dual > wide angle
|
|
754
|
-
let session = AVCaptureDevice.DiscoverySession(
|
|
755
|
-
deviceTypes: [.builtInTripleCamera, .builtInDualCamera, .builtInWideAngleCamera],
|
|
756
|
-
mediaType: .video,
|
|
757
|
-
position: .back
|
|
758
|
-
)
|
|
759
|
-
return session.devices.first
|
|
760
|
-
}
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
---
|
|
764
|
-
|
|
765
|
-
## 10. Restricting Scan Region
|
|
766
|
-
|
|
767
|
-
Limit the metadata detection area to improve performance and UX:
|
|
768
|
-
|
|
769
|
-
```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)
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
Note that `rectOfInterest` uses the camera coordinate system (landscape, origin top-left). Convert from preview coordinates using `previewLayer.metadataOutputRectConverted(fromLayerRect:)`.
|
|
1
|
+
# Camera Capture
|
|
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
|
+
---
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
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)
|
|
19
|
+
|
|
20
|
+
## 1. Complete Camera Manager with Photo Capture
|
|
21
|
+
|
|
22
|
+
A full-featured camera model with photo capture using the delegate pattern.
|
|
23
|
+
|
|
24
|
+
```swift
|
|
25
|
+
import AVFoundation
|
|
26
|
+
import UIKit
|
|
27
|
+
|
|
28
|
+
@available(iOS 17.0, *)
|
|
29
|
+
@Observable
|
|
30
|
+
@MainActor
|
|
31
|
+
final class CameraManager: NSObject {
|
|
32
|
+
let session = AVCaptureSession()
|
|
33
|
+
|
|
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
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
session.beginConfiguration()
|
|
53
|
+
session.sessionPreset = .photo
|
|
54
|
+
|
|
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)
|
|
73
|
+
|
|
74
|
+
// Enable maximum quality (iOS 16+)
|
|
75
|
+
if let maxDimensions = photoOutput.maxPhotoDimensions(for: .photo) {
|
|
76
|
+
photoOutput.maxPhotoDimensions = maxDimensions
|
|
77
|
+
}
|
|
78
|
+
photoOutput.maxPhotoQualityPrioritization = .quality
|
|
79
|
+
|
|
80
|
+
session.commitConfiguration()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MARK: - Session Control
|
|
84
|
+
|
|
85
|
+
func start() {
|
|
86
|
+
guard !session.isRunning else { return }
|
|
87
|
+
Task.detached { [session] in
|
|
88
|
+
session.startRunning()
|
|
89
|
+
}
|
|
90
|
+
isRunning = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func stop() {
|
|
94
|
+
guard session.isRunning else { return }
|
|
95
|
+
Task.detached { [session] in
|
|
96
|
+
session.stopRunning()
|
|
97
|
+
}
|
|
98
|
+
isRunning = false
|
|
99
|
+
}
|
|
100
|
+
|
|
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
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
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)
|
|
147
|
+
}
|
|
148
|
+
return status == .authorized
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MARK: - AVCapturePhotoCaptureDelegate
|
|
153
|
+
|
|
154
|
+
@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()
|
|
162
|
+
Task { @MainActor in
|
|
163
|
+
lastCapturedPhoto = data
|
|
164
|
+
photoContinuation?.resume(returning: data)
|
|
165
|
+
photoContinuation = nil
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
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.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 2. Camera Preview (UIViewRepresentable)
|
|
176
|
+
|
|
177
|
+
```swift
|
|
178
|
+
import SwiftUI
|
|
179
|
+
import AVFoundation
|
|
180
|
+
|
|
181
|
+
struct CameraPreview: UIViewRepresentable {
|
|
182
|
+
let session: AVCaptureSession
|
|
183
|
+
|
|
184
|
+
func makeUIView(context: Context) -> CameraPreviewView {
|
|
185
|
+
let view = CameraPreviewView()
|
|
186
|
+
view.previewLayer.session = session
|
|
187
|
+
view.previewLayer.videoGravity = .resizeAspectFill
|
|
188
|
+
return view
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func updateUIView(_ uiView: CameraPreviewView, context: Context) {
|
|
192
|
+
if uiView.previewLayer.session !== session {
|
|
193
|
+
uiView.previewLayer.session = session
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
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 }
|
|
202
|
+
|
|
203
|
+
var previewLayer: AVCaptureVideoPreviewLayer {
|
|
204
|
+
layer as! AVCaptureVideoPreviewLayer
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Never add `AVCaptureVideoPreviewLayer` as a sublayer manually. Using `layerClass` avoids manual frame management in `layoutSubviews`.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 3. Complete Camera Screen in SwiftUI
|
|
214
|
+
|
|
215
|
+
```swift
|
|
216
|
+
import SwiftUI
|
|
217
|
+
|
|
218
|
+
@available(iOS 17.0, *)
|
|
219
|
+
struct CameraScreen: View {
|
|
220
|
+
@State private var camera = CameraManager()
|
|
221
|
+
@State private var showCapturedPhoto = false
|
|
222
|
+
|
|
223
|
+
var body: some View {
|
|
224
|
+
ZStack {
|
|
225
|
+
CameraPreview(session: camera.session)
|
|
226
|
+
.ignoresSafeArea()
|
|
227
|
+
|
|
228
|
+
VStack {
|
|
229
|
+
// Top controls
|
|
230
|
+
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
|
+
|
|
240
|
+
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())
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
.padding(.horizontal)
|
|
254
|
+
|
|
255
|
+
Spacer()
|
|
256
|
+
|
|
257
|
+
// Bottom controls
|
|
258
|
+
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(RoundedRectangle(cornerRadius: 8))
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
Color.clear.frame(width: 50, height: 50)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
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
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
Spacer()
|
|
286
|
+
|
|
287
|
+
Color.clear.frame(width: 50, height: 50)
|
|
288
|
+
}
|
|
289
|
+
.padding(.horizontal, 24)
|
|
290
|
+
.padding(.bottom, 32)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
.task {
|
|
294
|
+
await camera.configure()
|
|
295
|
+
camera.start()
|
|
296
|
+
}
|
|
297
|
+
.onDisappear {
|
|
298
|
+
camera.stop()
|
|
299
|
+
}
|
|
300
|
+
.sheet(isPresented: $showCapturedPhoto) {
|
|
301
|
+
if let data = camera.lastCapturedPhoto,
|
|
302
|
+
let uiImage = UIImage(data: data) {
|
|
303
|
+
Image(uiImage: uiImage)
|
|
304
|
+
.resizable()
|
|
305
|
+
.scaledToFit()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 4. Video Recording
|
|
315
|
+
|
|
316
|
+
Add `AVCaptureMovieFileOutput` for video capture. Video recording requires `NSMicrophoneUsageDescription` in Info.plist for audio.
|
|
317
|
+
|
|
318
|
+
```swift
|
|
319
|
+
import AVFoundation
|
|
320
|
+
|
|
321
|
+
@available(iOS 17.0, *)
|
|
322
|
+
@Observable
|
|
323
|
+
@MainActor
|
|
324
|
+
final class VideoRecorder: NSObject {
|
|
325
|
+
let session = AVCaptureSession()
|
|
326
|
+
|
|
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
|
+
}
|
|
339
|
+
|
|
340
|
+
session.beginConfiguration()
|
|
341
|
+
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
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
func stopRecording() async -> URL? {
|
|
382
|
+
guard isRecording else { return nil }
|
|
383
|
+
|
|
384
|
+
return await withCheckedContinuation { continuation in
|
|
385
|
+
videoContinuation = continuation
|
|
386
|
+
movieOutput.stopRecording()
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
func start() {
|
|
391
|
+
Task.detached { [session] in session.startRunning() }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
func stop() {
|
|
395
|
+
Task.detached { [session] in session.stopRunning() }
|
|
396
|
+
}
|
|
397
|
+
|
|
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)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return videoGranted
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@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
|
+
) {
|
|
425
|
+
Task { @MainActor in
|
|
426
|
+
isRecording = false
|
|
427
|
+
recordedVideoURL = error == nil ? outputFileURL : nil
|
|
428
|
+
videoContinuation?.resume(returning: error == nil ? outputFileURL : nil)
|
|
429
|
+
videoContinuation = nil
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
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
|
+
---
|
|
438
|
+
|
|
439
|
+
## 5. Flash and Torch Control
|
|
440
|
+
|
|
441
|
+
Flash applies to photo capture settings. Torch provides continuous illumination for video or preview.
|
|
442
|
+
|
|
443
|
+
```swift
|
|
444
|
+
import AVFoundation
|
|
445
|
+
|
|
446
|
+
func toggleTorch(on device: AVCaptureDevice, enabled: Bool) throws {
|
|
447
|
+
guard device.hasTorch, device.isTorchAvailable else { return }
|
|
448
|
+
|
|
449
|
+
try device.lockForConfiguration()
|
|
450
|
+
device.torchMode = enabled ? .on : .off
|
|
451
|
+
if enabled {
|
|
452
|
+
try device.setTorchModeOn(level: AVCaptureDevice.maxAvailableTorchLevel)
|
|
453
|
+
}
|
|
454
|
+
device.unlockForConfiguration()
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Always wrap device configuration in `lockForConfiguration()` / `unlockForConfiguration()`. Multiple clients may attempt to configure the device simultaneously.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 6. Focus and Exposure
|
|
463
|
+
|
|
464
|
+
Implement tap-to-focus by converting a SwiftUI tap location to the camera coordinate system.
|
|
465
|
+
|
|
466
|
+
```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
|
+
|
|
476
|
+
try device.lockForConfiguration()
|
|
477
|
+
|
|
478
|
+
if device.isFocusPointOfInterestSupported {
|
|
479
|
+
device.focusPointOfInterest = devicePoint
|
|
480
|
+
device.focusMode = .autoFocus
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if device.isExposurePointOfInterestSupported {
|
|
484
|
+
device.exposurePointOfInterest = devicePoint
|
|
485
|
+
device.exposureMode = .autoExpose
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
device.unlockForConfiguration()
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
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
|
|
503
|
+
|
|
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.
|
|
536
|
+
|
|
537
|
+
```swift
|
|
538
|
+
import AVFoundation
|
|
539
|
+
|
|
540
|
+
@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
|
+
|
|
554
|
+
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
|
+
}
|
|
563
|
+
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()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
func start() {
|
|
579
|
+
scannedCode = nil
|
|
580
|
+
isScanning = true
|
|
581
|
+
Task.detached { [session] in session.startRunning() }
|
|
582
|
+
}
|
|
583
|
+
|
|
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
|
+
|
|
608
|
+
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, 60)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
.task {
|
|
645
|
+
await scanner.configure()
|
|
646
|
+
scanner.start()
|
|
647
|
+
}
|
|
648
|
+
.onDisappear {
|
|
649
|
+
scanner.stop()
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
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
|
+
---
|
|
658
|
+
|
|
659
|
+
## 8. Camera Preview Orientation
|
|
660
|
+
|
|
661
|
+
Handle device rotation so the preview and captured photos have correct orientation.
|
|
662
|
+
|
|
663
|
+
### Preview Layer Rotation
|
|
664
|
+
|
|
665
|
+
```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
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let settings = AVCapturePhotoSettings()
|
|
709
|
+
photoOutput.capturePhoto(with: settings, delegate: self)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private func currentVideoRotationAngle() -> CGFloat {
|
|
713
|
+
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
|
|
714
|
+
return 90
|
|
715
|
+
}
|
|
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
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
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).
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## 9. Dual Camera and Device Discovery
|
|
731
|
+
|
|
732
|
+
Select specific camera hardware using `AVCaptureDevice.DiscoverySession`.
|
|
733
|
+
|
|
734
|
+
```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
|
+
],
|
|
746
|
+
mediaType: .video,
|
|
747
|
+
position: .unspecified
|
|
748
|
+
)
|
|
749
|
+
return discoverySession.devices
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
func preferredBackCamera() -> AVCaptureDevice? {
|
|
753
|
+
// Prefer triple > dual > wide angle
|
|
754
|
+
let session = AVCaptureDevice.DiscoverySession(
|
|
755
|
+
deviceTypes: [.builtInTripleCamera, .builtInDualCamera, .builtInWideAngleCamera],
|
|
756
|
+
mediaType: .video,
|
|
757
|
+
position: .back
|
|
758
|
+
)
|
|
759
|
+
return session.devices.first
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## 10. Restricting Scan Region
|
|
766
|
+
|
|
767
|
+
Limit the metadata detection area to improve performance and UX:
|
|
768
|
+
|
|
769
|
+
```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)
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
Note that `rectOfInterest` uses the camera coordinate system (landscape, origin top-left). Convert from preview coordinates using `previewLayer.metadataOutputRectConverted(fromLayerRect:)`.
|