[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6E66RN3ZGS0F402SPD7GQC0":3,"skill-same:01M6E66RN3ZGS0F402SPD7GQC0":66},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":34,"linkOnly":35,"markdownContent":36,"markdownBody":37,"frontMatter":38,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":46,"repositoryHtmlUrl":47,"repositoryHomepageUrl":48,"ownerHtmlUrl":49,"ownerBlogUrl":6,"ownerDescription":50,"updatedAt":51,"install":52,"resources":65},"01M6E66RN3ZGS0F402SPD7GQC0","axiom-camera-capture",null,"AVCaptureSession, camera preview, photo capture, video recording, RotationCoordinator, session interruptions, deferred processing, capture responsiveness, zero-shutter-lag, photoQualityPrioritization, front camera mirroring","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-camera-capture\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33],"capture","camera","axiom camera capture","macos","ZeRO","ios","Video","session","preview","processing","2026-11-10 10:12:30",false,"---\nname: axiom-camera-capture\ndescription: AVCaptureSession, camera preview, photo capture, video recording, RotationCoordinator, session interruptions, deferred processing, capture responsiveness, zero-shutter-lag, photoQualityPrioritization, front camera mirroring\nlicense: MIT\ncompatibility: iOS 17+, iPadOS 17+, macOS 14+, tvOS 17+, axiom-visionOS 1+\nmetadata:\n  version: \"1.0.0\"\n  last-updated: \"2026-01-03\"\n---\n\n# Camera Capture with AVFoundation\n\nGuides you through implementing camera capture: session setup, photo capture, video recording, responsive capture UX, rotation handling, and session lifecycle management.\n\n## When to Use This Skill\n\nUse when you need to:\n- ☑ Build a custom camera UI (not system picker)\n- ☑ Capture photos with quality\u002Fspeed tradeoffs\n- ☑ Record video with audio\n- ☑ Handle device rotation correctly (RotationCoordinator)\n- ☑ Make capture feel responsive (zero-shutter-lag)\n- ☑ Handle session interruptions (phone calls, multitasking)\n- ☑ Switch between front\u002Fback cameras\n- ☑ Configure capture quality and resolution\n\n## Example Prompts\n\n\"How do I set up a camera preview in SwiftUI?\"\n\"My camera freezes when I get a phone call\"\n\"The photo preview is rotated wrong on front camera\"\n\"How do I make photo capture feel instant?\"\n\"Should I use deferred processing?\"\n\"My camera takes too long to capture\"\n\"How do I switch between front and back cameras?\"\n\"How do I record video with audio?\"\n\n## Red Flags\n\nSigns you're making this harder than it needs to be:\n\n- ❌ Calling `startRunning()` on main thread (blocks UI for seconds)\n- ❌ Using deprecated `videoOrientation` instead of RotationCoordinator (iOS 17+)\n- ❌ Not observing session interruptions (app freezes on phone call)\n- ❌ Creating new AVCaptureSession for each capture (expensive)\n- ❌ Using `.photo` preset for video (wrong format)\n- ❌ Ignoring `photoQualityPrioritization` (slow captures)\n- ❌ Not handling `.notAuthorized` permission state\n- ❌ Modifying session without `beginConfiguration()`\u002F`commitConfiguration()`\n- ❌ Using UIImagePickerController for custom camera UI (limited control)\n\n## Mandatory First Steps\n\nBefore implementing any camera feature:\n\n### 1. Choose Your Capture Mode\n\n```\nWhat do you need?\n\n┌─ Just let user pick a photo?\n│  └─ Don't use AVFoundation - use PHPicker or PhotosPicker\n│     See: \u002Fskill axiom-photo-library\n│\n├─ Simple photo\u002Fvideo capture with system UI?\n│  └─ UIImagePickerController (but limited customization)\n│\n├─ Custom camera UI with photo capture?\n│  └─ AVCaptureSession + AVCapturePhotoOutput\n│     → Continue with this skill\n│\n├─ Custom camera UI with video recording?\n│  └─ AVCaptureSession + AVCaptureMovieFileOutput\n│     → Continue with this skill\n│\n└─ Both photo and video in same session?\n   └─ AVCaptureSession + both outputs\n      → Continue with this skill\n```\n\n### 2. Request Camera Permission\n\n```swift\nimport AVFoundation\n\nfunc requestCameraAccess() async -> Bool {\n    let status = AVCaptureDevice.authorizationStatus(for: .video)\n\n    switch status {\n    case .authorized:\n        return true\n    case .notDetermined:\n        return await AVCaptureDevice.requestAccess(for: .video)\n    case .denied, .restricted:\n        \u002F\u002F Show settings prompt\n        return false\n    @unknown default:\n        return false\n    }\n}\n```\n\n**Info.plist required**:\n```xml\n\u003Ckey>NSCameraUsageDescription\u003C\u002Fkey>\n\u003Cstring>Take photos and videos\u003C\u002Fstring>\n```\n\nFor audio (video recording):\n```xml\n\u003Ckey>NSMicrophoneUsageDescription\u003C\u002Fkey>\n\u003Cstring>Record audio with video\u003C\u002Fstring>\n```\n\n### 3. Understand Session Architecture\n\n```\nAVCaptureSession\n    ├─ Inputs\n    │   ├─ AVCaptureDeviceInput (camera)\n    │   └─ AVCaptureDeviceInput (microphone, for video)\n    │\n    ├─ Outputs\n    │   ├─ AVCapturePhotoOutput (photos)\n    │   ├─ AVCaptureMovieFileOutput (video files)\n    │   └─ AVCaptureVideoDataOutput (raw frames)\n    │\n    └─ Connections (automatic between compatible input\u002Foutput)\n```\n\n**Key rule**: All session configuration happens on a **dedicated serial queue**, never main thread.\n\n## Core Patterns\n\n### Pattern 1: Basic Session Setup\n\n**Use case**: Set up camera preview with photo capture capability.\n\n```swift\nimport AVFoundation\n\nclass CameraManager: NSObject {\n    let session = AVCaptureSession()\n    let photoOutput = AVCapturePhotoOutput()\n\n    \u002F\u002F CRITICAL: Dedicated serial queue for session work\n    private let sessionQueue = DispatchQueue(label: \"camera.session\")\n\n    func setupSession() {\n        sessionQueue.async { [self] in\n            session.beginConfiguration()\n            defer { session.commitConfiguration() }\n\n            \u002F\u002F 1. Set session preset\n            session.sessionPreset = .photo\n\n            \u002F\u002F 2. Add camera input\n            guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera,\n                                                        for: .video,\n                                                        position: .back),\n                  let input = try? AVCaptureDeviceInput(device: camera),\n                  session.canAddInput(input) else {\n                return\n            }\n            session.addInput(input)\n\n            \u002F\u002F 3. Add photo output\n            guard session.canAddOutput(photoOutput) else { return }\n            session.addOutput(photoOutput)\n\n            \u002F\u002F 4. Configure photo output\n            photoOutput.isHighResolutionCaptureEnabled = true\n            photoOutput.maxPhotoQualityPrioritization = .quality\n        }\n    }\n\n    func startSession() {\n        sessionQueue.async { [self] in\n            if !session.isRunning {\n                session.startRunning()  \u002F\u002F Blocking call - never on main thread!\n            }\n        }\n    }\n\n    func stopSession() {\n        sessionQueue.async { [self] in\n            if session.isRunning {\n                session.stopRunning()\n            }\n        }\n    }\n}\n```\n\n**Cost**: 30 min implementation\n\n### Pattern 2: SwiftUI Camera Preview\n\n**Use case**: Display camera preview in SwiftUI view.\n\n```swift\nimport SwiftUI\nimport AVFoundation\n\nstruct CameraPreview: UIViewRepresentable {\n    let session: AVCaptureSession\n\n    func makeUIView(context: Context) -> PreviewView {\n        let view = PreviewView()\n        view.previewLayer.session = session\n        view.previewLayer.videoGravity = .resizeAspectFill\n        return view\n    }\n\n    func updateUIView(_ uiView: PreviewView, context: Context) {}\n\n    class PreviewView: UIView {\n        override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }\n        var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }\n    }\n}\n\n\u002F\u002F Usage in SwiftUI\nstruct CameraView: View {\n    @StateObject private var camera = CameraManager()\n\n    var body: some View {\n        CameraPreview(session: camera.session)\n            .ignoresSafeArea()\n            .onAppear { camera.startSession() }\n            .onDisappear { camera.stopSession() }\n    }\n}\n```\n\n**Cost**: 20 min implementation\n\n### Pattern 3: Rotation Handling with RotationCoordinator (iOS 17+)\n\n**Use case**: Keep preview and captured photos correctly oriented regardless of device rotation.\n\n**Why RotationCoordinator**: Deprecated `videoOrientation` requires manual observation of device orientation. RotationCoordinator automatically tracks gravity and provides angles.\n\n```swift\nimport AVFoundation\n\nclass CameraManager {\n    private var rotationCoordinator: AVCaptureDevice.RotationCoordinator?\n    private var rotationObservation: NSKeyValueObservation?\n\n    func setupRotationCoordinator(device: AVCaptureDevice, previewLayer: AVCaptureVideoPreviewLayer) {\n        \u002F\u002F Create coordinator with device and preview layer\n        rotationCoordinator = AVCaptureDevice.RotationCoordinator(\n            device: device,\n            previewLayer: previewLayer\n        )\n\n        \u002F\u002F Observe preview rotation changes\n        rotationObservation = rotationCoordinator?.observe(\n            \\.videoRotationAngleForHorizonLevelPreview,\n            options: [.new]\n        ) { [weak previewLayer] coordinator, _ in\n            \u002F\u002F Update preview layer rotation on main thread\n            DispatchQueue.main.async {\n                previewLayer?.connection?.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelPreview\n            }\n        }\n\n        \u002F\u002F Set initial rotation\n        previewLayer.connection?.videoRotationAngle = rotationCoordinator!.videoRotationAngleForHorizonLevelPreview\n    }\n\n    func captureRotationAngle() -> CGFloat {\n        \u002F\u002F Use this angle when capturing photos\n        rotationCoordinator?.videoRotationAngleForHorizonLevelCapture ?? 0\n    }\n}\n```\n\n**When capturing**:\n```swift\nfunc capturePhoto() {\n    let settings = AVCapturePhotoSettings()\n\n    \u002F\u002F Apply rotation angle from coordinator\n    if let connection = photoOutput.connection(with: .video) {\n        connection.videoRotationAngle = captureRotationAngle()\n    }\n\n    photoOutput.capturePhoto(with: settings, delegate: self)\n}\n```\n\n**Cost**: 45 min implementation, prevents 2+ hours debugging rotation issues\n\n### Pattern 4: Responsive Capture Pipeline (iOS 17+)\n\n**Use case**: Make photo capture feel instant with zero-shutter-lag, overlapping captures, and responsive button states.\n\n**iOS 17+ introduces four complementary APIs** that work together for maximum responsiveness:\n\n#### 4a. Zero Shutter Lag\n\nUses a ring buffer of recent frames to \"time travel\" back to the exact moment you tapped the shutter. Enabled automatically for iOS 17+ apps.\n\n```swift\n\u002F\u002F Check if supported for current format\nif photoOutput.isZeroShutterLagSupported {\n    \u002F\u002F Enabled by default for apps linking iOS 17+\n    \u002F\u002F Opt out if causing issues:\n    \u002F\u002F photoOutput.isZeroShutterLagEnabled = false\n}\n```\n\n**Why it matters**: Without ZSL, there's a delay between tap and frame capture. For action shots, the moment is already over.\n\n**Requirements**: iPhone XS and newer. Does NOT apply to flash captures, manual exposure, bracketed captures, or constituent photo delivery.\n\n#### 4b. Responsive Capture (Overlapping Captures)\n\nAllows a new capture to start while the previous one is still processing:\n\n```swift\n\u002F\u002F Check support first\nif photoOutput.isZeroShutterLagSupported {\n    photoOutput.isZeroShutterLagEnabled = true  \u002F\u002F Required for responsive capture\n\n    if photoOutput.isResponsiveCaptureSupported {\n        photoOutput.isResponsiveCaptureEnabled = true\n    }\n}\n```\n\n**Tradeoff**: Increases peak memory usage. If your app is memory-constrained, consider leaving disabled.\n\n**Requirements**: A12 Bionic (iPhone XS) and newer.\n\n#### 4c. Fast Capture Prioritization\n\nAutomatically adapts quality when taking multiple photos rapidly (like burst mode):\n\n```swift\nif photoOutput.isFastCapturePrioritizationSupported {\n    photoOutput.isFastCapturePrioritizationEnabled = true\n    \u002F\u002F When enabled, rapid captures use \"balanced\" quality instead of \"quality\"\n    \u002F\u002F to maintain consistent shot-to-shot time\n}\n```\n\n**When to enable**: User-facing toggle (\"Prioritize Faster Shooting\" in Camera.app). Off by default because it reduces quality.\n\n#### 4d. Readiness Coordinator (Button State Management)\n\n**Critical for UX**: Provides synchronous updates for shutter button state without async lag.\n\n```swift\nclass CameraManager {\n    private var readinessCoordinator: AVCapturePhotoOutputReadinessCoordinator!\n\n    func setupReadinessCoordinator() {\n        readinessCoordinator = AVCapturePhotoOutputReadinessCoordinator(photoOutput: photoOutput)\n        readinessCoordinator.delegate = self\n    }\n\n    func capturePhoto() {\n        var settings = AVCapturePhotoSettings()\n        settings.photoQualityPrioritization = .balanced\n\n        \u002F\u002F Tell coordinator to track this capture BEFORE calling capturePhoto\n        readinessCoordinator.startTrackingCaptureRequest(using: settings)\n\n        photoOutput.capturePhoto(with: settings, delegate: self)\n    }\n}\n\nextension CameraManager: AVCapturePhotoOutputReadinessCoordinatorDelegate {\n    func readinessCoordinator(_ coordinator: AVCapturePhotoOutputReadinessCoordinator,\n                              captureReadinessDidChange captureReadiness: AVCapturePhotoOutput.CaptureReadiness) {\n        DispatchQueue.main.async {\n            switch captureReadiness {\n            case .ready:\n                self.shutterButton.isEnabled = true\n                self.shutterButton.alpha = 1.0\n\n            case .notReadyMomentarily:\n                \u002F\u002F Brief delay - disable to prevent double-tap\n                self.shutterButton.isEnabled = false\n\n            case .notReadyWaitingForCapture:\n                \u002F\u002F Flash is firing - dim button\n                self.shutterButton.alpha = 0.5\n\n            case .notReadyWaitingForProcessing:\n                \u002F\u002F Processing previous photo - show spinner\n                self.showProcessingIndicator()\n\n            case .sessionNotRunning:\n                self.shutterButton.isEnabled = false\n\n            @unknown default:\n                break\n            }\n        }\n    }\n}\n```\n\n**Why use Readiness Coordinator**: Without it, you'd need to track capture state manually and users might spam the shutter button during processing.\n\n#### Quality Prioritization (Baseline)\n\nStill useful even without the new APIs:\n\n```swift\nfunc capturePhoto() {\n    var settings = AVCapturePhotoSettings()\n\n    \u002F\u002F Speed vs Quality tradeoff\n    \u002F\u002F .speed     - Fastest capture, lower quality\n    \u002F\u002F .balanced  - Good default\n    \u002F\u002F .quality   - Best quality, may have delay\n    settings.photoQualityPrioritization = .speed\n\n    \u002F\u002F For specific use cases:\n    \u002F\u002F - Social sharing: .speed (users expect instant)\n    \u002F\u002F - Document scanning: .quality (accuracy matters)\n    \u002F\u002F - General photography: .balanced\n\n    photoOutput.capturePhoto(with: settings, delegate: self)\n}\n```\n\n**Deferred Processing (iOS 17+)**:\n\nFor maximum responsiveness, capture returns immediately with proxy image, full Deep Fusion processing happens in background:\n\n```swift\n\u002F\u002F Check support and enable deferred processing\nif photoOutput.isAutoDeferredPhotoDeliverySupported {\n    photoOutput.isAutoDeferredPhotoDeliveryEnabled = true\n}\n```\n\n**Delegate callbacks with deferred processing**:\n\n```swift\n\u002F\u002F Called for BOTH regular photos AND deferred proxies\nfunc photoOutput(_ output: AVCapturePhotoOutput,\n                 didFinishProcessingPhoto photo: AVCapturePhoto,\n                 error: Error?) {\n    guard error == nil else { return }\n\n    \u002F\u002F Non-deferred photo - save directly\n    if !photo.isRawPhoto, let data = photo.fileDataRepresentation() {\n        savePhotoToLibrary(data)\n    }\n}\n\n\u002F\u002F Called ONLY for deferred proxies - save to PhotoKit for later processing\nfunc photoOutput(_ output: AVCapturePhotoOutput,\n                 didFinishCapturingDeferredPhotoProxy deferredPhotoProxy: AVCaptureDeferredPhotoProxy,\n                 error: Error?) {\n    guard error == nil else { return }\n\n    \u002F\u002F CRITICAL: Save proxy to library ASAP before app is backgrounded\n    \u002F\u002F App may be force-quit if memory pressure is high during backgrounding\n    guard let proxyData = deferredPhotoProxy.fileDataRepresentation() else { return }\n\n    Task {\n        try await PHPhotoLibrary.shared().performChanges {\n            let request = PHAssetCreationRequest.forAsset()\n            \u002F\u002F Use .photoProxy resource type - triggers deferred processing in Photos\n            request.addResource(with: .photoProxy, data: proxyData, options: nil)\n        }\n    }\n}\n```\n\n**When final processing happens**:\n- On-demand when image is requested from PhotoKit\n- Or automatically when device is idle (plugged in, not in use)\n\n**Fetching images with deferred processing awareness**:\n\n```swift\n\u002F\u002F Request with secondary degraded image for smoother UX\nlet options = PHImageRequestOptions()\noptions.allowSecondaryDegradedImage = true  \u002F\u002F New in iOS 17\n\nPHImageManager.default().requestImage(\n    for: asset,\n    targetSize: targetSize,\n    contentMode: .aspectFill,\n    options: options\n) { image, info in\n    let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool ?? false\n\n    if isDegraded {\n        \u002F\u002F First: Low quality (immediate)\n        \u002F\u002F Second: Medium quality (new - while processing)\n        \u002F\u002F Third callback will be final quality\n        self.showTemporaryImage(image)\n    } else {\n        \u002F\u002F Final quality - processing complete\n        self.showFinalImage(image)\n    }\n}\n```\n\n**Requirements**: iPhone 11 Pro and newer. Not used for flash captures or formats that don't benefit from extended processing.\n\n**Important considerations**:\n- Can't apply pixel buffer customizations (filters, metadata changes) to deferred photos\n- Use PhotoKit adjustments after processing for edits\n- Get proxy into library ASAP - limited time when backgrounded\n\n**Cost**: 1 hour implementation, prevents \"camera feels slow\" complaints\n\n### Pattern 5: Session Interruption Handling\n\n**Use case**: Handle phone calls, multitasking, system camera usage.\n\n```swift\nclass CameraManager {\n    private var interruptionObservers: [NSObjectProtocol] = []\n\n    func setupInterruptionHandling() {\n        \u002F\u002F Session was interrupted\n        let interruptedObserver = NotificationCenter.default.addObserver(\n            forName: .AVCaptureSessionWasInterrupted,\n            object: session,\n            queue: .main\n        ) { [weak self] notification in\n            guard let reason = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int,\n                  let interruptionReason = AVCaptureSession.InterruptionReason(rawValue: reason) else {\n                return\n            }\n\n            switch interruptionReason {\n            case .videoDeviceNotAvailableInBackground:\n                \u002F\u002F App went to background - normal, will resume\n                self?.showPausedOverlay()\n\n            case .audioDeviceInUseByAnotherClient:\n                \u002F\u002F Another app using audio\n                self?.showInterruptedBanner(\"Audio in use by another app\")\n\n            case .videoDeviceInUseByAnotherClient:\n                \u002F\u002F Another app using camera\n                self?.showInterruptedBanner(\"Camera in use by another app\")\n\n            case .videoDeviceNotAvailableWithMultipleForegroundApps:\n                \u002F\u002F Split View\u002FSlide Over - camera not available\n                self?.showInterruptedBanner(\"Camera unavailable in Split View\")\n\n            case .videoDeviceNotAvailableDueToSystemPressure:\n                \u002F\u002F Thermal state - reduce quality or stop\n                self?.handleThermalPressure()\n\n            @unknown default:\n                self?.showInterruptedBanner(\"Camera interrupted\")\n            }\n        }\n        interruptionObservers.append(interruptedObserver)\n\n        \u002F\u002F Session interruption ended\n        let endedObserver = NotificationCenter.default.addObserver(\n            forName: .AVCaptureSessionInterruptionEnded,\n            object: session,\n            queue: .main\n        ) { [weak self] _ in\n            self?.hideInterruptedBanner()\n            self?.hidePausedOverlay()\n            \u002F\u002F Session automatically resumes - no need to call startRunning()\n        }\n        interruptionObservers.append(endedObserver)\n    }\n\n    deinit {\n        interruptionObservers.forEach { NotificationCenter.default.removeObserver($0) }\n    }\n}\n```\n\n**Cost**: 30 min implementation, prevents \"camera freezes\" bug reports\n\n### Pattern 6: Camera Switching (Front\u002FBack)\n\n**Use case**: Toggle between front and back cameras.\n\n```swift\nfunc switchCamera() {\n    sessionQueue.async { [self] in\n        guard let currentInput = session.inputs.first as? AVCaptureDeviceInput else {\n            return\n        }\n\n        let currentPosition = currentInput.device.position\n        let newPosition: AVCaptureDevice.Position = currentPosition == .back ? .front : .back\n\n        guard let newDevice = AVCaptureDevice.default(\n            .builtInWideAngleCamera,\n            for: .video,\n            position: newPosition\n        ) else {\n            return\n        }\n\n        session.beginConfiguration()\n        defer { session.commitConfiguration() }\n\n        \u002F\u002F Remove old input\n        session.removeInput(currentInput)\n\n        \u002F\u002F Add new input\n        do {\n            let newInput = try AVCaptureDeviceInput(device: newDevice)\n            if session.canAddInput(newInput) {\n                session.addInput(newInput)\n\n                \u002F\u002F Update rotation coordinator for new device\n                if let previewLayer = previewLayer {\n                    setupRotationCoordinator(device: newDevice, previewLayer: previewLayer)\n                }\n            } else {\n                \u002F\u002F Fallback: restore old input\n                session.addInput(currentInput)\n            }\n        } catch {\n            session.addInput(currentInput)\n        }\n    }\n}\n```\n\n**Front camera mirroring**: Front camera preview is mirrored by default (matches user expectation). Captured photos are NOT mirrored (correct for sharing). This is intentional.\n\n**Cost**: 20 min implementation\n\n### Pattern 7: Video Recording\n\n**Use case**: Record video with audio to file.\n\n```swift\nclass CameraManager: NSObject {\n    let movieOutput = AVCaptureMovieFileOutput()\n    private var currentRecordingURL: URL?\n\n    func setupVideoRecording() {\n        sessionQueue.async { [self] in\n            session.beginConfiguration()\n            defer { session.commitConfiguration() }\n\n            \u002F\u002F Set video preset\n            session.sessionPreset = .high  \u002F\u002F Or .hd1920x1080, .hd4K3840x2160\n\n            \u002F\u002F Add microphone input\n            if let microphone = AVCaptureDevice.default(for: .audio),\n               let audioInput = try? AVCaptureDeviceInput(device: microphone),\n               session.canAddInput(audioInput) {\n                session.addInput(audioInput)\n            }\n\n            \u002F\u002F Add movie output\n            if session.canAddOutput(movieOutput) {\n                session.addOutput(movieOutput)\n            }\n        }\n    }\n\n    func startRecording() {\n        guard !movieOutput.isRecording else { return }\n\n        let outputURL = FileManager.default.temporaryDirectory\n            .appendingPathComponent(UUID().uuidString)\n            .appendingPathExtension(\"mov\")\n\n        currentRecordingURL = outputURL\n\n        \u002F\u002F Apply rotation\n        if let connection = movieOutput.connection(with: .video) {\n            connection.videoRotationAngle = captureRotationAngle()\n        }\n\n        movieOutput.startRecording(to: outputURL, recordingDelegate: self)\n    }\n\n    func stopRecording() {\n        guard movieOutput.isRecording else { return }\n        movieOutput.stopRecording()\n    }\n}\n\nextension CameraManager: AVCaptureFileOutputRecordingDelegate {\n    func fileOutput(_ output: AVCaptureFileOutput,\n                    didFinishRecordingTo outputFileURL: URL,\n                    from connections: [AVCaptureConnection],\n                    error: Error?) {\n        if let error = error {\n            print(\"Recording error: \\(error)\")\n            return\n        }\n\n        \u002F\u002F Video saved to outputFileURL\n        saveVideoToPhotoLibrary(outputFileURL)\n    }\n}\n```\n\n**Cost**: 45 min implementation\n\n## Anti-Patterns\n\n### Anti-Pattern 1: Session Work on Main Thread\n\n**Wrong**:\n```swift\nfunc startCamera() {\n    session.startRunning()  \u002F\u002F Blocks UI for 1-3 seconds!\n}\n```\n\n**Right**:\n```swift\nfunc startCamera() {\n    sessionQueue.async { [self] in\n        session.startRunning()\n    }\n}\n```\n\n**Why it matters**: `startRunning()` is blocking. On main thread, UI freezes.\n\n### Anti-Pattern 2: Using Deprecated videoOrientation\n\n**Wrong** (pre-iOS 17):\n```swift\n\u002F\u002F Manually tracking orientation\nNotificationCenter.default.addObserver(\n    forName: UIDevice.orientationDidChangeNotification,\n    object: nil,\n    queue: .main\n) { _ in\n    \u002F\u002F Manual rotation logic...\n}\n```\n\n**Right** (iOS 17+):\n```swift\nlet coordinator = AVCaptureDevice.RotationCoordinator(device: camera, previewLayer: preview)\n\u002F\u002F Automatically tracks gravity, provides angles\n```\n\n**Why it matters**: RotationCoordinator handles edge cases (face-up, face-down) that manual tracking misses.\n\n### Anti-Pattern 3: Ignoring Session Interruptions\n\n**Wrong**:\n```swift\n\u002F\u002F No interruption handling - camera freezes on phone call\n```\n\n**Right**:\n```swift\nNotificationCenter.default.addObserver(\n    forName: .AVCaptureSessionWasInterrupted,\n    object: session,\n    queue: .main\n) { notification in\n    \u002F\u002F Show UI feedback\n}\n```\n\n**Why it matters**: Without handling, camera appears frozen when interrupted.\n\n### Anti-Pattern 4: Modifying Session Without Configuration Block\n\n**Wrong**:\n```swift\nsession.removeInput(oldInput)\nsession.addInput(newInput)  \u002F\u002F May fail mid-stream\n```\n\n**Right**:\n```swift\nsession.beginConfiguration()\nsession.removeInput(oldInput)\nsession.addInput(newInput)\nsession.commitConfiguration()  \u002F\u002F Atomic change\n```\n\n**Why it matters**: Without configuration block, session may enter invalid state between calls.\n\n## Pressure Scenarios\n\n### Scenario 1: \"Just Make the Camera Work by Friday\"\n\n**Context**: Product wants camera feature shipped. You're considering skipping interruption handling.\n\n**Pressure**: \"It works when I test it, let's ship.\"\n\n**Reality**: First user who gets a phone call while using camera will see frozen UI. App Store review may catch this.\n\n**Correct action**:\n1. Implement interruption handling (30 min)\n2. Test by calling your test device during camera use\n3. Verify UI shows appropriate feedback\n\n**Push-back template**: \"Camera captures work, but the app freezes if a phone call comes in. I need 30 minutes to handle interruptions properly and avoid 1-star reviews.\"\n\n### Scenario 2: \"The Camera is Too Slow\"\n\n**Context**: QA reports photo capture feels sluggish. PM wants it \"instant like the system camera.\"\n\n**Pressure**: \"Just make it faster somehow.\"\n\n**Reality**: Default settings prioritize quality over speed. System camera uses deferred processing.\n\n**Correct action**:\n1. Set `photoQualityPrioritization = .speed` for social\u002Fsharing use cases\n2. Consider deferred processing for maximum responsiveness\n3. Show capture animation immediately (before processing completes)\n\n**Push-back template**: \"We're currently optimizing for image quality. I can make capture feel instant by prioritizing speed and showing the preview immediately while processing continues in background. This is what the system Camera app does.\"\n\n### Scenario 3: \"Why is the Front Camera Photo Mirrored?\"\n\n**Context**: Designer reports front camera photos look \"wrong\" - they're not mirrored like the preview.\n\n**Pressure**: \"The preview shows it one way, the photo should match.\"\n\n**Reality**: Preview is mirrored (user expectation - like a mirror). Photo is NOT mirrored (correct for sharing - text reads correctly). This is intentional behavior matching system camera.\n\n**Correct action**:\n1. Explain this is Apple's standard behavior\n2. If business requires mirrored photos (selfie apps), manually mirror in post-processing\n3. Never mirror the preview differently than expected\n\n**Push-back template**: \"This is intentional Apple behavior. The preview is mirrored like a mirror so users can frame themselves, but the captured photo is unmirrored so text reads correctly when shared. We can add optional mirroring in post-processing if our use case requires it.\"\n\n## Checklist\n\nBefore shipping camera features:\n\n**Session Setup**:\n- ☑ All session work on dedicated serial queue\n- ☑ `startRunning()` never called on main thread\n- ☑ Session preset matches use case (`.photo` for photos, `.high` for video)\n- ☑ Configuration changes wrapped in `beginConfiguration()`\u002F`commitConfiguration()`\n\n**Permissions**:\n- ☑ Camera permission requested before session setup\n- ☑ `NSCameraUsageDescription` in Info.plist\n- ☑ `NSMicrophoneUsageDescription` if recording audio\n- ☑ Graceful handling of denied permission\n\n**Rotation**:\n- ☑ RotationCoordinator used (not deprecated videoOrientation)\n- ☑ Preview layer rotation updated via observation\n- ☑ Capture rotation angle applied when taking photos\n- ☑ Tested in all orientations (portrait, landscape, face-up)\n\n**Responsiveness**:\n- ☑ photoQualityPrioritization set appropriately for use case\n- ☑ Capture button shows immediate feedback\n- ☑ Deferred processing considered for maximum speed\n\n**Interruptions**:\n- ☑ Session interruption observer registered\n- ☑ UI feedback shown when interrupted\n- ☑ Tested with incoming phone call\n- ☑ Tested in Split View (iPad)\n\n**Camera Switching**:\n- ☑ Front\u002Fback switch updates rotation coordinator\n- ☑ Switch happens on session queue\n- ☑ Fallback if new camera unavailable\n\n**Video Recording** (if applicable):\n- ☑ Microphone input added\n- ☑ Recording delegate handles completion\n- ☑ File cleanup for temporary recordings\n\n## Resources\n\n**WWDC**: 2021-10247, 2023-10105\n\n**Docs**: \u002Favfoundation\u002Favcapturesession, \u002Favfoundation\u002Favcapturedevice\u002Frotationcoordinator, \u002Favfoundation\u002Favcapturephotosettings, \u002Favfoundation\u002Favcapturephotooutputreadinesscoordinator\n\n**Skills**: axiom-camera-capture-ref, axiom-camera-capture-diag, axiom-photo-library\n\n","# Camera Capture with AVFoundation\n\nGuides you through implementing camera capture: session setup, photo capture, video recording, responsive capture UX, rotation handling, and session lifecycle management.\n\n## When to Use This Skill\n\nUse when you need to:\n- ☑ Build a custom camera UI (not system picker)\n- ☑ Capture photos with quality\u002Fspeed tradeoffs\n- ☑ Record video with audio\n- ☑ Handle device rotation correctly (RotationCoordinator)\n- ☑ Make capture feel responsive (zero-shutter-lag)\n- ☑ Handle session interruptions (phone calls, multitasking)\n- ☑ Switch between front\u002Fback cameras\n- ☑ Configure capture quality and resolution\n\n## Example Prompts\n\n\"How do I set up a camera preview in SwiftUI?\"\n\"My camera freezes when I get a phone call\"\n\"The photo preview is rotated wrong on front camera\"\n\"How do I make photo capture feel instant?\"\n\"Should I use deferred processing?\"\n\"My camera takes too long to capture\"\n\"How do I switch between front and back cameras?\"\n\"How do I record video with audio?\"\n\n## Red Flags\n\nSigns you're making this harder than it needs to be:\n\n- ❌ Calling `startRunning()` on main thread (blocks UI for seconds)\n- ❌ Using deprecated `videoOrientation` instead of RotationCoordinator (iOS 17+)\n- ❌ Not observing session interruptions (app freezes on phone call)\n- ❌ Creating new AVCaptureSession for each capture (expensive)\n- ❌ Using `.photo` preset for video (wrong format)\n- ❌ Ignoring `photoQualityPrioritization` (slow captures)\n- ❌ Not handling `.notAuthorized` permission state\n- ❌ Modifying session without `beginConfiguration()`\u002F`commitConfiguration()`\n- ❌ Using UIImagePickerController for custom camera UI (limited control)\n\n## Mandatory First Steps\n\nBefore implementing any camera feature:\n\n### 1. Choose Your Capture Mode\n\n```\nWhat do you need?\n\n┌─ Just let user pick a photo?\n│  └─ Don't use AVFoundation - use PHPicker or PhotosPicker\n│     See: \u002Fskill axiom-photo-library\n│\n├─ Simple photo\u002Fvideo capture with system UI?\n│  └─ UIImagePickerController (but limited customization)\n│\n├─ Custom camera UI with photo capture?\n│  └─ AVCaptureSession + AVCapturePhotoOutput\n│     → Continue with this skill\n│\n├─ Custom camera UI with video recording?\n│  └─ AVCaptureSession + AVCaptureMovieFileOutput\n│     → Continue with this skill\n│\n└─ Both photo and video in same session?\n   └─ AVCaptureSession + both outputs\n      → Continue with this skill\n```\n\n### 2. Request Camera Permission\n\n```swift\nimport AVFoundation\n\nfunc requestCameraAccess() async -> Bool {\n    let status = AVCaptureDevice.authorizationStatus(for: .video)\n\n    switch status {\n    case .authorized:\n        return true\n    case .notDetermined:\n        return await AVCaptureDevice.requestAccess(for: .video)\n    case .denied, .restricted:\n        \u002F\u002F Show settings prompt\n        return false\n    @unknown default:\n        return false\n    }\n}\n```\n\n**Info.plist required**:\n```xml\n\u003Ckey>NSCameraUsageDescription\u003C\u002Fkey>\n\u003Cstring>Take photos and videos\u003C\u002Fstring>\n```\n\nFor audio (video recording):\n```xml\n\u003Ckey>NSMicrophoneUsageDescription\u003C\u002Fkey>\n\u003Cstring>Record audio with video\u003C\u002Fstring>\n```\n\n### 3. Understand Session Architecture\n\n```\nAVCaptureSession\n    ├─ Inputs\n    │   ├─ AVCaptureDeviceInput (camera)\n    │   └─ AVCaptureDeviceInput (microphone, for video)\n    │\n    ├─ Outputs\n    │   ├─ AVCapturePhotoOutput (photos)\n    │   ├─ AVCaptureMovieFileOutput (video files)\n    │   └─ AVCaptureVideoDataOutput (raw frames)\n    │\n    └─ Connections (automatic between compatible input\u002Foutput)\n```\n\n**Key rule**: All session configuration happens on a **dedicated serial queue**, never main thread.\n\n## Core Patterns\n\n### Pattern 1: Basic Session Setup\n\n**Use case**: Set up camera preview with photo capture capability.\n\n```swift\nimport AVFoundation\n\nclass CameraManager: NSObject {\n    let session = AVCaptureSession()\n    let photoOutput = AVCapturePhotoOutput()\n\n    \u002F\u002F CRITICAL: Dedicated serial queue for session work\n    private let sessionQueue = DispatchQueue(label: \"camera.session\")\n\n    func setupSession() {\n        sessionQueue.async { [self] in\n            session.beginConfiguration()\n            defer { session.commitConfiguration() }\n\n            \u002F\u002F 1. Set session preset\n            session.sessionPreset = .photo\n\n            \u002F\u002F 2. Add camera input\n            guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera,\n                                                        for: .video,\n                                                        position: .back),\n                  let input = try? AVCaptureDeviceInput(device: camera),\n                  session.canAddInput(input) else {\n                return\n            }\n            session.addInput(input)\n\n            \u002F\u002F 3. Add photo output\n            guard session.canAddOutput(photoOutput) else { return }\n            session.addOutput(photoOutput)\n\n            \u002F\u002F 4. Configure photo output\n            photoOutput.isHighResolutionCaptureEnabled = true\n            photoOutput.maxPhotoQualityPrioritization = .quality\n        }\n    }\n\n    func startSession() {\n        sessionQueue.async { [self] in\n            if !session.isRunning {\n                session.startRunning()  \u002F\u002F Blocking call - never on main thread!\n            }\n        }\n    }\n\n    func stopSession() {\n        sessionQueue.async { [self] in\n            if session.isRunning {\n                session.stopRunning()\n            }\n        }\n    }\n}\n```\n\n**Cost**: 30 min implementation\n\n### Pattern 2: SwiftUI Camera Preview\n\n**Use case**: Display camera preview in SwiftUI view.\n\n```swift\nimport SwiftUI\nimport AVFoundation\n\nstruct CameraPreview: UIViewRepresentable {\n    let session: AVCaptureSession\n\n    func makeUIView(context: Context) -> PreviewView {\n        let view = PreviewView()\n        view.previewLayer.session = session\n        view.previewLayer.videoGravity = .resizeAspectFill\n        return view\n    }\n\n    func updateUIView(_ uiView: PreviewView, context: Context) {}\n\n    class PreviewView: UIView {\n        override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }\n        var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }\n    }\n}\n\n\u002F\u002F Usage in SwiftUI\nstruct CameraView: View {\n    @StateObject private var camera = CameraManager()\n\n    var body: some View {\n        CameraPreview(session: camera.session)\n            .ignoresSafeArea()\n            .onAppear { camera.startSession() }\n            .onDisappear { camera.stopSession() }\n    }\n}\n```\n\n**Cost**: 20 min implementation\n\n### Pattern 3: Rotation Handling with RotationCoordinator (iOS 17+)\n\n**Use case**: Keep preview and captured photos correctly oriented regardless of device rotation.\n\n**Why RotationCoordinator**: Deprecated `videoOrientation` requires manual observation of device orientation. RotationCoordinator automatically tracks gravity and provides angles.\n\n```swift\nimport AVFoundation\n\nclass CameraManager {\n    private var rotationCoordinator: AVCaptureDevice.RotationCoordinator?\n    private var rotationObservation: NSKeyValueObservation?\n\n    func setupRotationCoordinator(device: AVCaptureDevice, previewLayer: AVCaptureVideoPreviewLayer) {\n        \u002F\u002F Create coordinator with device and preview layer\n        rotationCoordinator = AVCaptureDevice.RotationCoordinator(\n            device: device,\n            previewLayer: previewLayer\n        )\n\n        \u002F\u002F Observe preview rotation changes\n        rotationObservation = rotationCoordinator?.observe(\n            \\.videoRotationAngleForHorizonLevelPreview,\n            options: [.new]\n        ) { [weak previewLayer] coordinator, _ in\n            \u002F\u002F Update preview layer rotation on main thread\n            DispatchQueue.main.async {\n                previewLayer?.connection?.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelPreview\n            }\n        }\n\n        \u002F\u002F Set initial rotation\n        previewLayer.connection?.videoRotationAngle = rotationCoordinator!.videoRotationAngleForHorizonLevelPreview\n    }\n\n    func captureRotationAngle() -> CGFloat {\n        \u002F\u002F Use this angle when capturing photos\n        rotationCoordinator?.videoRotationAngleForHorizonLevelCapture ?? 0\n    }\n}\n```\n\n**When capturing**:\n```swift\nfunc capturePhoto() {\n    let settings = AVCapturePhotoSettings()\n\n    \u002F\u002F Apply rotation angle from coordinator\n    if let connection = photoOutput.connection(with: .video) {\n        connection.videoRotationAngle = captureRotationAngle()\n    }\n\n    photoOutput.capturePhoto(with: settings, delegate: self)\n}\n```\n\n**Cost**: 45 min implementation, prevents 2+ hours debugging rotation issues\n\n### Pattern 4: Responsive Capture Pipeline (iOS 17+)\n\n**Use case**: Make photo capture feel instant with zero-shutter-lag, overlapping captures, and responsive button states.\n\n**iOS 17+ introduces four complementary APIs** that work together for maximum responsiveness:\n\n#### 4a. Zero Shutter Lag\n\nUses a ring buffer of recent frames to \"time travel\" back to the exact moment you tapped the shutter. Enabled automatically for iOS 17+ apps.\n\n```swift\n\u002F\u002F Check if supported for current format\nif photoOutput.isZeroShutterLagSupported {\n    \u002F\u002F Enabled by default for apps linking iOS 17+\n    \u002F\u002F Opt out if causing issues:\n    \u002F\u002F photoOutput.isZeroShutterLagEnabled = false\n}\n```\n\n**Why it matters**: Without ZSL, there's a delay between tap and frame capture. For action shots, the moment is already over.\n\n**Requirements**: iPhone XS and newer. Does NOT apply to flash captures, manual exposure, bracketed captures, or constituent photo delivery.\n\n#### 4b. Responsive Capture (Overlapping Captures)\n\nAllows a new capture to start while the previous one is still processing:\n\n```swift\n\u002F\u002F Check support first\nif photoOutput.isZeroShutterLagSupported {\n    photoOutput.isZeroShutterLagEnabled = true  \u002F\u002F Required for responsive capture\n\n    if photoOutput.isResponsiveCaptureSupported {\n        photoOutput.isResponsiveCaptureEnabled = true\n    }\n}\n```\n\n**Tradeoff**: Increases peak memory usage. If your app is memory-constrained, consider leaving disabled.\n\n**Requirements**: A12 Bionic (iPhone XS) and newer.\n\n#### 4c. Fast Capture Prioritization\n\nAutomatically adapts quality when taking multiple photos rapidly (like burst mode):\n\n```swift\nif photoOutput.isFastCapturePrioritizationSupported {\n    photoOutput.isFastCapturePrioritizationEnabled = true\n    \u002F\u002F When enabled, rapid captures use \"balanced\" quality instead of \"quality\"\n    \u002F\u002F to maintain consistent shot-to-shot time\n}\n```\n\n**When to enable**: User-facing toggle (\"Prioritize Faster Shooting\" in Camera.app). Off by default because it reduces quality.\n\n#### 4d. Readiness Coordinator (Button State Management)\n\n**Critical for UX**: Provides synchronous updates for shutter button state without async lag.\n\n```swift\nclass CameraManager {\n    private var readinessCoordinator: AVCapturePhotoOutputReadinessCoordinator!\n\n    func setupReadinessCoordinator() {\n        readinessCoordinator = AVCapturePhotoOutputReadinessCoordinator(photoOutput: photoOutput)\n        readinessCoordinator.delegate = self\n    }\n\n    func capturePhoto() {\n        var settings = AVCapturePhotoSettings()\n        settings.photoQualityPrioritization = .balanced\n\n        \u002F\u002F Tell coordinator to track this capture BEFORE calling capturePhoto\n        readinessCoordinator.startTrackingCaptureRequest(using: settings)\n\n        photoOutput.capturePhoto(with: settings, delegate: self)\n    }\n}\n\nextension CameraManager: AVCapturePhotoOutputReadinessCoordinatorDelegate {\n    func readinessCoordinator(_ coordinator: AVCapturePhotoOutputReadinessCoordinator,\n                              captureReadinessDidChange captureReadiness: AVCapturePhotoOutput.CaptureReadiness) {\n        DispatchQueue.main.async {\n            switch captureReadiness {\n            case .ready:\n                self.shutterButton.isEnabled = true\n                self.shutterButton.alpha = 1.0\n\n            case .notReadyMomentarily:\n                \u002F\u002F Brief delay - disable to prevent double-tap\n                self.shutterButton.isEnabled = false\n\n            case .notReadyWaitingForCapture:\n                \u002F\u002F Flash is firing - dim button\n                self.shutterButton.alpha = 0.5\n\n            case .notReadyWaitingForProcessing:\n                \u002F\u002F Processing previous photo - show spinner\n                self.showProcessingIndicator()\n\n            case .sessionNotRunning:\n                self.shutterButton.isEnabled = false\n\n            @unknown default:\n                break\n            }\n        }\n    }\n}\n```\n\n**Why use Readiness Coordinator**: Without it, you'd need to track capture state manually and users might spam the shutter button during processing.\n\n#### Quality Prioritization (Baseline)\n\nStill useful even without the new APIs:\n\n```swift\nfunc capturePhoto() {\n    var settings = AVCapturePhotoSettings()\n\n    \u002F\u002F Speed vs Quality tradeoff\n    \u002F\u002F .speed     - Fastest capture, lower quality\n    \u002F\u002F .balanced  - Good default\n    \u002F\u002F .quality   - Best quality, may have delay\n    settings.photoQualityPrioritization = .speed\n\n    \u002F\u002F For specific use cases:\n    \u002F\u002F - Social sharing: .speed (users expect instant)\n    \u002F\u002F - Document scanning: .quality (accuracy matters)\n    \u002F\u002F - General photography: .balanced\n\n    photoOutput.capturePhoto(with: settings, delegate: self)\n}\n```\n\n**Deferred Processing (iOS 17+)**:\n\nFor maximum responsiveness, capture returns immediately with proxy image, full Deep Fusion processing happens in background:\n\n```swift\n\u002F\u002F Check support and enable deferred processing\nif photoOutput.isAutoDeferredPhotoDeliverySupported {\n    photoOutput.isAutoDeferredPhotoDeliveryEnabled = true\n}\n```\n\n**Delegate callbacks with deferred processing**:\n\n```swift\n\u002F\u002F Called for BOTH regular photos AND deferred proxies\nfunc photoOutput(_ output: AVCapturePhotoOutput,\n                 didFinishProcessingPhoto photo: AVCapturePhoto,\n                 error: Error?) {\n    guard error == nil else { return }\n\n    \u002F\u002F Non-deferred photo - save directly\n    if !photo.isRawPhoto, let data = photo.fileDataRepresentation() {\n        savePhotoToLibrary(data)\n    }\n}\n\n\u002F\u002F Called ONLY for deferred proxies - save to PhotoKit for later processing\nfunc photoOutput(_ output: AVCapturePhotoOutput,\n                 didFinishCapturingDeferredPhotoProxy deferredPhotoProxy: AVCaptureDeferredPhotoProxy,\n                 error: Error?) {\n    guard error == nil else { return }\n\n    \u002F\u002F CRITICAL: Save proxy to library ASAP before app is backgrounded\n    \u002F\u002F App may be force-quit if memory pressure is high during backgrounding\n    guard let proxyData = deferredPhotoProxy.fileDataRepresentation() else { return }\n\n    Task {\n        try await PHPhotoLibrary.shared().performChanges {\n            let request = PHAssetCreationRequest.forAsset()\n            \u002F\u002F Use .photoProxy resource type - triggers deferred processing in Photos\n            request.addResource(with: .photoProxy, data: proxyData, options: nil)\n        }\n    }\n}\n```\n\n**When final processing happens**:\n- On-demand when image is requested from PhotoKit\n- Or automatically when device is idle (plugged in, not in use)\n\n**Fetching images with deferred processing awareness**:\n\n```swift\n\u002F\u002F Request with secondary degraded image for smoother UX\nlet options = PHImageRequestOptions()\noptions.allowSecondaryDegradedImage = true  \u002F\u002F New in iOS 17\n\nPHImageManager.default().requestImage(\n    for: asset,\n    targetSize: targetSize,\n    contentMode: .aspectFill,\n    options: options\n) { image, info in\n    let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool ?? false\n\n    if isDegraded {\n        \u002F\u002F First: Low quality (immediate)\n        \u002F\u002F Second: Medium quality (new - while processing)\n        \u002F\u002F Third callback will be final quality\n        self.showTemporaryImage(image)\n    } else {\n        \u002F\u002F Final quality - processing complete\n        self.showFinalImage(image)\n    }\n}\n```\n\n**Requirements**: iPhone 11 Pro and newer. Not used for flash captures or formats that don't benefit from extended processing.\n\n**Important considerations**:\n- Can't apply pixel buffer customizations (filters, metadata changes) to deferred photos\n- Use PhotoKit adjustments after processing for edits\n- Get proxy into library ASAP - limited time when backgrounded\n\n**Cost**: 1 hour implementation, prevents \"camera feels slow\" complaints\n\n### Pattern 5: Session Interruption Handling\n\n**Use case**: Handle phone calls, multitasking, system camera usage.\n\n```swift\nclass CameraManager {\n    private var interruptionObservers: [NSObjectProtocol] = []\n\n    func setupInterruptionHandling() {\n        \u002F\u002F Session was interrupted\n        let interruptedObserver = NotificationCenter.default.addObserver(\n            forName: .AVCaptureSessionWasInterrupted,\n            object: session,\n            queue: .main\n        ) { [weak self] notification in\n            guard let reason = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int,\n                  let interruptionReason = AVCaptureSession.InterruptionReason(rawValue: reason) else {\n                return\n            }\n\n            switch interruptionReason {\n            case .videoDeviceNotAvailableInBackground:\n                \u002F\u002F App went to background - normal, will resume\n                self?.showPausedOverlay()\n\n            case .audioDeviceInUseByAnotherClient:\n                \u002F\u002F Another app using audio\n                self?.showInterruptedBanner(\"Audio in use by another app\")\n\n            case .videoDeviceInUseByAnotherClient:\n                \u002F\u002F Another app using camera\n                self?.showInterruptedBanner(\"Camera in use by another app\")\n\n            case .videoDeviceNotAvailableWithMultipleForegroundApps:\n                \u002F\u002F Split View\u002FSlide Over - camera not available\n                self?.showInterruptedBanner(\"Camera unavailable in Split View\")\n\n            case .videoDeviceNotAvailableDueToSystemPressure:\n                \u002F\u002F Thermal state - reduce quality or stop\n                self?.handleThermalPressure()\n\n            @unknown default:\n                self?.showInterruptedBanner(\"Camera interrupted\")\n            }\n        }\n        interruptionObservers.append(interruptedObserver)\n\n        \u002F\u002F Session interruption ended\n        let endedObserver = NotificationCenter.default.addObserver(\n            forName: .AVCaptureSessionInterruptionEnded,\n            object: session,\n            queue: .main\n        ) { [weak self] _ in\n            self?.hideInterruptedBanner()\n            self?.hidePausedOverlay()\n            \u002F\u002F Session automatically resumes - no need to call startRunning()\n        }\n        interruptionObservers.append(endedObserver)\n    }\n\n    deinit {\n        interruptionObservers.forEach { NotificationCenter.default.removeObserver($0) }\n    }\n}\n```\n\n**Cost**: 30 min implementation, prevents \"camera freezes\" bug reports\n\n### Pattern 6: Camera Switching (Front\u002FBack)\n\n**Use case**: Toggle between front and back cameras.\n\n```swift\nfunc switchCamera() {\n    sessionQueue.async { [self] in\n        guard let currentInput = session.inputs.first as? AVCaptureDeviceInput else {\n            return\n        }\n\n        let currentPosition = currentInput.device.position\n        let newPosition: AVCaptureDevice.Position = currentPosition == .back ? .front : .back\n\n        guard let newDevice = AVCaptureDevice.default(\n            .builtInWideAngleCamera,\n            for: .video,\n            position: newPosition\n        ) else {\n            return\n        }\n\n        session.beginConfiguration()\n        defer { session.commitConfiguration() }\n\n        \u002F\u002F Remove old input\n        session.removeInput(currentInput)\n\n        \u002F\u002F Add new input\n        do {\n            let newInput = try AVCaptureDeviceInput(device: newDevice)\n            if session.canAddInput(newInput) {\n                session.addInput(newInput)\n\n                \u002F\u002F Update rotation coordinator for new device\n                if let previewLayer = previewLayer {\n                    setupRotationCoordinator(device: newDevice, previewLayer: previewLayer)\n                }\n            } else {\n                \u002F\u002F Fallback: restore old input\n                session.addInput(currentInput)\n            }\n        } catch {\n            session.addInput(currentInput)\n        }\n    }\n}\n```\n\n**Front camera mirroring**: Front camera preview is mirrored by default (matches user expectation). Captured photos are NOT mirrored (correct for sharing). This is intentional.\n\n**Cost**: 20 min implementation\n\n### Pattern 7: Video Recording\n\n**Use case**: Record video with audio to file.\n\n```swift\nclass CameraManager: NSObject {\n    let movieOutput = AVCaptureMovieFileOutput()\n    private var currentRecordingURL: URL?\n\n    func setupVideoRecording() {\n        sessionQueue.async { [self] in\n            session.beginConfiguration()\n            defer { session.commitConfiguration() }\n\n            \u002F\u002F Set video preset\n            session.sessionPreset = .high  \u002F\u002F Or .hd1920x1080, .hd4K3840x2160\n\n            \u002F\u002F Add microphone input\n            if let microphone = AVCaptureDevice.default(for: .audio),\n               let audioInput = try? AVCaptureDeviceInput(device: microphone),\n               session.canAddInput(audioInput) {\n                session.addInput(audioInput)\n            }\n\n            \u002F\u002F Add movie output\n            if session.canAddOutput(movieOutput) {\n                session.addOutput(movieOutput)\n            }\n        }\n    }\n\n    func startRecording() {\n        guard !movieOutput.isRecording else { return }\n\n        let outputURL = FileManager.default.temporaryDirectory\n            .appendingPathComponent(UUID().uuidString)\n            .appendingPathExtension(\"mov\")\n\n        currentRecordingURL = outputURL\n\n        \u002F\u002F Apply rotation\n        if let connection = movieOutput.connection(with: .video) {\n            connection.videoRotationAngle = captureRotationAngle()\n        }\n\n        movieOutput.startRecording(to: outputURL, recordingDelegate: self)\n    }\n\n    func stopRecording() {\n        guard movieOutput.isRecording else { return }\n        movieOutput.stopRecording()\n    }\n}\n\nextension CameraManager: AVCaptureFileOutputRecordingDelegate {\n    func fileOutput(_ output: AVCaptureFileOutput,\n                    didFinishRecordingTo outputFileURL: URL,\n                    from connections: [AVCaptureConnection],\n                    error: Error?) {\n        if let error = error {\n            print(\"Recording error: \\(error)\")\n            return\n        }\n\n        \u002F\u002F Video saved to outputFileURL\n        saveVideoToPhotoLibrary(outputFileURL)\n    }\n}\n```\n\n**Cost**: 45 min implementation\n\n## Anti-Patterns\n\n### Anti-Pattern 1: Session Work on Main Thread\n\n**Wrong**:\n```swift\nfunc startCamera() {\n    session.startRunning()  \u002F\u002F Blocks UI for 1-3 seconds!\n}\n```\n\n**Right**:\n```swift\nfunc startCamera() {\n    sessionQueue.async { [self] in\n        session.startRunning()\n    }\n}\n```\n\n**Why it matters**: `startRunning()` is blocking. On main thread, UI freezes.\n\n### Anti-Pattern 2: Using Deprecated videoOrientation\n\n**Wrong** (pre-iOS 17):\n```swift\n\u002F\u002F Manually tracking orientation\nNotificationCenter.default.addObserver(\n    forName: UIDevice.orientationDidChangeNotification,\n    object: nil,\n    queue: .main\n) { _ in\n    \u002F\u002F Manual rotation logic...\n}\n```\n\n**Right** (iOS 17+):\n```swift\nlet coordinator = AVCaptureDevice.RotationCoordinator(device: camera, previewLayer: preview)\n\u002F\u002F Automatically tracks gravity, provides angles\n```\n\n**Why it matters**: RotationCoordinator handles edge cases (face-up, face-down) that manual tracking misses.\n\n### Anti-Pattern 3: Ignoring Session Interruptions\n\n**Wrong**:\n```swift\n\u002F\u002F No interruption handling - camera freezes on phone call\n```\n\n**Right**:\n```swift\nNotificationCenter.default.addObserver(\n    forName: .AVCaptureSessionWasInterrupted,\n    object: session,\n    queue: .main\n) { notification in\n    \u002F\u002F Show UI feedback\n}\n```\n\n**Why it matters**: Without handling, camera appears frozen when interrupted.\n\n### Anti-Pattern 4: Modifying Session Without Configuration Block\n\n**Wrong**:\n```swift\nsession.removeInput(oldInput)\nsession.addInput(newInput)  \u002F\u002F May fail mid-stream\n```\n\n**Right**:\n```swift\nsession.beginConfiguration()\nsession.removeInput(oldInput)\nsession.addInput(newInput)\nsession.commitConfiguration()  \u002F\u002F Atomic change\n```\n\n**Why it matters**: Without configuration block, session may enter invalid state between calls.\n\n## Pressure Scenarios\n\n### Scenario 1: \"Just Make the Camera Work by Friday\"\n\n**Context**: Product wants camera feature shipped. You're considering skipping interruption handling.\n\n**Pressure**: \"It works when I test it, let's ship.\"\n\n**Reality**: First user who gets a phone call while using camera will see frozen UI. App Store review may catch this.\n\n**Correct action**:\n1. Implement interruption handling (30 min)\n2. Test by calling your test device during camera use\n3. Verify UI shows appropriate feedback\n\n**Push-back template**: \"Camera captures work, but the app freezes if a phone call comes in. I need 30 minutes to handle interruptions properly and avoid 1-star reviews.\"\n\n### Scenario 2: \"The Camera is Too Slow\"\n\n**Context**: QA reports photo capture feels sluggish. PM wants it \"instant like the system camera.\"\n\n**Pressure**: \"Just make it faster somehow.\"\n\n**Reality**: Default settings prioritize quality over speed. System camera uses deferred processing.\n\n**Correct action**:\n1. Set `photoQualityPrioritization = .speed` for social\u002Fsharing use cases\n2. Consider deferred processing for maximum responsiveness\n3. Show capture animation immediately (before processing completes)\n\n**Push-back template**: \"We're currently optimizing for image quality. I can make capture feel instant by prioritizing speed and showing the preview immediately while processing continues in background. This is what the system Camera app does.\"\n\n### Scenario 3: \"Why is the Front Camera Photo Mirrored?\"\n\n**Context**: Designer reports front camera photos look \"wrong\" - they're not mirrored like the preview.\n\n**Pressure**: \"The preview shows it one way, the photo should match.\"\n\n**Reality**: Preview is mirrored (user expectation - like a mirror). Photo is NOT mirrored (correct for sharing - text reads correctly). This is intentional behavior matching system camera.\n\n**Correct action**:\n1. Explain this is Apple's standard behavior\n2. If business requires mirrored photos (selfie apps), manually mirror in post-processing\n3. Never mirror the preview differently than expected\n\n**Push-back template**: \"This is intentional Apple behavior. The preview is mirrored like a mirror so users can frame themselves, but the captured photo is unmirrored so text reads correctly when shared. We can add optional mirroring in post-processing if our use case requires it.\"\n\n## Checklist\n\nBefore shipping camera features:\n\n**Session Setup**:\n- ☑ All session work on dedicated serial queue\n- ☑ `startRunning()` never called on main thread\n- ☑ Session preset matches use case (`.photo` for photos, `.high` for video)\n- ☑ Configuration changes wrapped in `beginConfiguration()`\u002F`commitConfiguration()`\n\n**Permissions**:\n- ☑ Camera permission requested before session setup\n- ☑ `NSCameraUsageDescription` in Info.plist\n- ☑ `NSMicrophoneUsageDescription` if recording audio\n- ☑ Graceful handling of denied permission\n\n**Rotation**:\n- ☑ RotationCoordinator used (not deprecated videoOrientation)\n- ☑ Preview layer rotation updated via observation\n- ☑ Capture rotation angle applied when taking photos\n- ☑ Tested in all orientations (portrait, landscape, face-up)\n\n**Responsiveness**:\n- ☑ photoQualityPrioritization set appropriately for use case\n- ☑ Capture button shows immediate feedback\n- ☑ Deferred processing considered for maximum speed\n\n**Interruptions**:\n- ☑ Session interruption observer registered\n- ☑ UI feedback shown when interrupted\n- ☑ Tested with incoming phone call\n- ☑ Tested in Split View (iPad)\n\n**Camera Switching**:\n- ☑ Front\u002Fback switch updates rotation coordinator\n- ☑ Switch happens on session queue\n- ☑ Fallback if new camera unavailable\n\n**Video Recording** (if applicable):\n- ☑ Microphone input added\n- ☑ Recording delegate handles completion\n- ☑ File cleanup for temporary recordings\n\n## Resources\n\n**WWDC**: 2021-10247, 2023-10105\n\n**Docs**: \u002Favfoundation\u002Favcapturesession, \u002Favfoundation\u002Favcapturedevice\u002Frotationcoordinator, \u002Favfoundation\u002Favcapturephotosettings, \u002Favfoundation\u002Favcapturephotooutputreadinesscoordinator\n\n**Skills**: axiom-camera-capture-ref, axiom-camera-capture-diag, axiom-photo-library\n\n",{"name":5,"license":15,"metadata":39,"description":7,"compatibility":42},{"version":40,"last-updated":41},"1.0.0","2026-01-03","iOS 17+, iPadOS 17+, macOS 14+, tvOS 17+, axiom-visionOS 1+","main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-camera-capture",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":53,"prompt":63,"zipAvailable":64},[54,57,60],{"tool":55,"command":56},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill axiom-camera-capture",{"tool":58,"command":59},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill axiom-camera-capture",{"tool":61,"command":62},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill axiom-camera-capture","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-camera-capture\u002FSKILL.md",true,[],[67,79,89,98,104,111],{"skillNo":68,"name":69,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":70,"categories":71,"keywords":75,"publishedAt":34},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[72],{"categoryNo":73,"categoryName":74,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[76,77,78],"model","model hierarchy","model hierarchy skill",{"skillNo":80,"name":81,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":85,"publishedAt":34},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[84],{"categoryNo":73,"categoryName":74,"taxonomyType":21,"taxonomyTypeDesc":22},[86,87,88],"MCP","zoom","zoom mcp",{"skillNo":90,"name":91,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":92,"categories":93,"keywords":94,"publishedAt":34},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[95,96,97,87],"sdk","zoom apps sdk","apps",{"skillNo":99,"name":100,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":101,"categories":102,"keywords":103,"publishedAt":34},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[100],{"skillNo":105,"name":106,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":110,"publishedAt":34},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[109],{"categoryNo":73,"categoryName":74,"taxonomyType":21,"taxonomyTypeDesc":22},[106],{"skillNo":112,"name":113,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":116,"publishedAt":34},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[113]]