mcpField
A
axiom-camera-capture

axiom-camera-capture

github.com/charleswiltgen开发1.2k235 次安装今天更新
代码仓库

关于 axiom-camera-capture

面向 Apple 应用开发者的 AVFoundation 相机采集技能,适用于构建自定义相机体验。内容涵盖会话设置、照片与视频采集、旋转处理、响应速度、会话中断和相机切换。

主要功能

  1. 01配置采集会话与预览
  2. 02拍摄照片并录制带音频视频
  3. 03使用 RotationCoordinator 处理旋转
  4. 04提升采集响应速度
  5. 05处理会话中断与相机切换

适用场景

  1. 01构建自定义 SwiftUI 相机预览
  2. 02实现速度与画质可权衡的拍照功能
  3. 03录制带音频的视频
  4. 04修复旋转或中断处理问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-camera-capture

安装后在 Claude Code 中输入/axiom-camera-capture确认已加载。

详细介绍

使用 AVFoundation 捕获相机画面

本指南将引导你实现相机捕获功能:会话设置、照片捕获、视频录制、响应迅速的捕获体验、旋转处理以及会话生命周期管理。

何时使用此技能

在以下情况下使用:

  • ☑ 构建自定义相机 UI(而非系统选择器)
  • ☑ 在画质和速度之间权衡并捕获照片
  • ☑ 录制带音频的视频
  • ☑ 正确处理设备旋转(RotationCoordinator)
  • ☑ 让捕获体验响应迅速(零快门延迟)
  • ☑ 处理会话中断(电话、多任务处理)
  • ☑ 在前置和后置相机之间切换
  • ☑ 配置捕获画质和分辨率

示例提示词

“如何在 SwiftUI 中设置相机预览?” “我的相机在接到电话时会卡住” “前置相机拍出的照片预览方向不对” “如何让照片捕获感觉即时完成?” “我应该使用延迟处理吗?” “我的相机捕获照片耗时太长” “如何在前置和后置相机之间切换?” “如何录制带音频的视频?”

危险信号

以下迹象表明,你可能让事情变得比必要的更复杂:

  • ❌ 在主线程上调用 startRunning()(会阻塞 UI 数秒)
  • ❌ 使用已弃用的 videoOrientation,而不使用 RotationCoordinator(iOS 17+)
  • ❌ 未监听会话中断(接到电话时应用会卡住)
  • ❌ 每次捕获都创建新的 AVCaptureSession(开销很大)
  • ❌ 视频使用 .photo 预设(格式错误)
  • ❌ 忽略 photoQualityPrioritization(捕获速度慢)
  • ❌ 未处理 .notAuthorized 权限状态
  • ❌ 未使用 beginConfiguration()/commitConfiguration() 修改会话
  • ❌ 使用 UIImagePickerController 构建自定义相机 UI(控制能力有限)

必须先完成的步骤

实现任何相机功能之前:

1. 选择捕获模式
What do you need?

┌─ Just let user pick a photo?
│  └─ Don't use AVFoundation - use PHPicker or PhotosPicker
│     See: /skill axiom-photo-library
│
├─ Simple photo/video capture with system UI?
│  └─ UIImagePickerController (but limited customization)
│
├─ Custom camera UI with photo capture?
│  └─ AVCaptureSession + AVCapturePhotoOutput
│     → Continue with this skill
│
├─ Custom camera UI with video recording?
│  └─ AVCaptureSession + AVCaptureMovieFileOutput
│     → Continue with this skill
│
└─ Both photo and video in same session?
   └─ AVCaptureSession + both outputs
      → Continue with this skill
2. 请求相机权限
import AVFoundation

func requestCameraAccess() async -> Bool {
    let status = AVCaptureDevice.authorizationStatus(for: .video)

    switch status {
    case .authorized:
        return true
    case .notDetermined:
        return await AVCaptureDevice.requestAccess(for: .video)
    case .denied, .restricted:
        // Show settings prompt
        return false
    @unknown default:
        return false
    }
}

需要在 Info.plist 中配置:

<key>NSCameraUsageDescription</key>
<string>Take photos and videos</string>

用于音频(视频录制):

<key>NSMicrophoneUsageDescription</key>
<string>Record audio with video</string>
3. 了解会话架构
AVCaptureSession
    ├─ Inputs
    │   ├─ AVCaptureDeviceInput (camera)
    │   └─ AVCaptureDeviceInput (microphone, for video)
    │
    ├─ Outputs
    │   ├─ AVCapturePhotoOutput (photos)
    │   ├─ AVCaptureMovieFileOutput (video files)
    │   └─ AVCaptureVideoDataOutput (raw frames)
    │
    └─ Connections (automatic between compatible input/output)

关键规则:所有会话配置都必须在专用串行队列上执行,绝不能在主线程上执行。

核心模式

模式 1:基本会话设置

适用场景:设置相机预览,并具备照片捕获功能。

import AVFoundation

class CameraManager: NSObject {
    let session = AVCaptureSession()
    let photoOutput = AVCapturePhotoOutput()

    // CRITICAL: Dedicated serial queue for session work
    private let sessionQueue = DispatchQueue(label: "camera.session")

    func setupSession() {
        sessionQueue.async { [self] in
            session.beginConfiguration()
            defer { session.commitConfiguration() }

            // 1. Set session preset
            session.sessionPreset = .photo

            // 2. Add camera input
            guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
                                                        for: .video,
                                                        position: .back),
                  let input = try? AVCaptureDeviceInput(device: camera),
                  session.canAddInput(input) else {
                return
            }
            session.addInput(input)

            // 3. Add photo output
            guard session.canAddOutput(photoOutput) else { return }
            session.addOutput(photoOutput)

            // 4. Configure photo output
            photoOutput.isHighResolutionCaptureEnabled = true
            photoOutput.maxPhotoQualityPrioritization = .quality
        }
    }

    func startSession() {
        sessionQueue.async { [self] in
            if !session.isRunning {
                session.startRunning()  // Blocking call - never on main thread!
            }
        }
    }

    func stopSession() {
        sessionQueue.async { [self] in
            if session.isRunning {
                session.stopRunning()
            }
        }
    }
}

耗时:30 分钟实现

模式 2:SwiftUI 相机预览

适用场景:在 SwiftUI 视图中显示相机预览。

import SwiftUI
import AVFoundation

struct CameraPreview: UIViewRepresentable {
    let session: AVCaptureSession

    func makeUIView(context: Context) -> PreviewView {
        let view = PreviewView()
        view.previewLayer.session = session
        view.previewLayer.videoGravity = .resizeAspectFill
        return view
    }

    func updateUIView(_ uiView: PreviewView, context: Context) {}

    class PreviewView: UIView {
        override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
        var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
    }
}

// Usage in SwiftUI
struct CameraView: View {
    @StateObject private var camera = CameraManager()

    var body: some View {
        CameraPreview(session: camera.session)
            .ignoresSafeArea()
            .onAppear { camera.startSession() }
            .onDisappear { camera.stopSession() }
    }
}

耗时:20 分钟实现

模式 3:使用 RotationCoordinator 处理旋转(iOS 17+)

适用场景:无论设备如何旋转,都确保预览和捕获的照片方向正确。

为什么使用 RotationCoordinator:已弃用的 videoOrientation 需要手动监听设备方向。RotationCoordinator 会自动跟踪重力并提供角度。

import AVFoundation

class CameraManager {
    private var rotationCoordinator: AVCaptureDevice.RotationCoordinator?
    private var rotationObservation: NSKeyValueObservation?

    func setupRotationCoordinator(device: AVCaptureDevice, previewLayer: AVCaptureVideoPreviewLayer) {
        // Create coordinator with device and preview layer
        rotationCoordinator = AVCaptureDevice.RotationCoordinator(
            device: device,
            previewLayer: previewLayer
        )

        // Observe preview rotation changes
        rotationObservation = rotationCoordinator?.observe(
            \.videoRotationAngleForHorizonLevelPreview,
            options: [.new]
        ) { [weak previewLayer] coordinator, _ in
            // Update preview layer rotation on main thread
            DispatchQueue.main.async {
                previewLayer?.connection?.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelPreview
            }
        }

        // Set initial rotation
        previewLayer.connection?.videoRotationAngle = rotationCoordinator!.videoRotationAngleForHorizonLevelPreview
    }

    func captureRotationAngle() -> CGFloat {
        // Use this angle when capturing photos
        rotationCoordinator?.videoRotationAngleForHorizonLevelCapture ?? 0
    }
}

捕获时:

func capturePhoto() {
    let settings = AVCapturePhotoSettings()

    // Apply rotation angle from coordinator
    if let connection = photoOutput.connection(with: .video) {
        connection.videoRotationAngle = captureRotationAngle()
    }

    photoOutput.capturePhoto(with: settings, delegate: self)
}

耗时:45 分钟实现,可避免花费 2 小时以上调试旋转问题

模式 4:响应迅速的捕获管线(iOS 17+)

适用场景:通过零快门延迟、重叠捕获和灵敏的按钮状态,让照片捕获感觉即时完成。

iOS 17+ 引入了四种互补 API,可协同工作以实现最高响应速度:

4a. 零快门延迟

使用近期帧的环形缓冲区,“穿越时间”回到你按下快门的准确时刻。iOS 17+ 应用会自动启用此功能。

// Check if supported for current format
if photoOutput.isZeroShutterLagSupported {
    // Enabled by default for apps linking iOS 17+
    // Opt out if causing issues:
    // photoOutput.isZeroShutterLagEnabled = false
}

重要性:如果没有 ZSL,从按下按钮到捕获帧之间会有延迟。拍摄动态场景时,精彩瞬间可能已经过去。

要求:iPhone XS 及更新机型。不适用于闪光灯捕获、手动曝光、包围式捕获或组成照片交付。

4b. 响应式捕获(重叠捕获)

允许在上一次捕获仍在处理时开始新的捕获:

// Check support first
if photoOutput.isZeroShutterLagSupported {
    photoOutput.isZeroShutterLagEnabled = true  // Required for responsive capture

    if photoOutput.isResponsiveCaptureSupported {
        photoOutput.isResponsiveCaptureEnabled = true
    }
}

权衡:会增加峰值内存使用量。如果应用受内存限制,可以考虑保持禁用。

要求:A12 Bionic(iPhone XS)及更新机型。

4c. 快速拍摄优先级

快速连续拍摄多张照片(类似连拍模式)时,自动调整画质:

if photoOutput.isFastCapturePrioritizationSupported {
    photoOutput.isFastCapturePrioritizationEnabled = true
    // When enabled, rapid captures use "balanced" quality instead of "quality"
    // to maintain consistent shot-to-shot time
}

启用时机:面向用户的开关(Camera.app 中的“优先快速拍摄”)。默认关闭,因为这会降低画质。

4d. 就绪状态协调器(按钮状态管理)

对用户体验至关重要:同步更新快门按钮状态,不会有异步延迟。

class CameraManager {
    private var readinessCoordinator: AVCapturePhotoOutputReadinessCoordinator!

    func setupReadinessCoordinator() {
        readinessCoordinator = AVCapturePhotoOutputReadinessCoordinator(photoOutput: photoOutput)
        readinessCoordinator.delegate = self
    }

    func capturePhoto() {
        var settings = AVCapturePhotoSettings()
        settings.photoQualityPrioritization = .balanced

        // Tell coordinator to track this capture BEFORE calling capturePhoto
        readinessCoordinator.startTrackingCaptureRequest(using: settings)

        photoOutput.capturePhoto(with: settings, delegate: self)
    }
}

extension CameraManager: AVCapturePhotoOutputReadinessCoordinatorDelegate {
    func readinessCoordinator(_ coordinator: AVCapturePhotoOutputReadinessCoordinator,
                              captureReadinessDidChange captureReadiness: AVCapturePhotoOutput.CaptureReadiness) {
        DispatchQueue.main.async {
            switch captureReadiness {
            case .ready:
                self.shutterButton.isEnabled = true
                self.shutterButton.alpha = 1.0

            case .notReadyMomentarily:
                // Brief delay - disable to prevent double-tap
                self.shutterButton.isEnabled = false

            case .notReadyWaitingForCapture:
                // Flash is firing - dim button
                self.shutterButton.alpha = 0.5

            case .notReadyWaitingForProcessing:
                // Processing previous photo - show spinner
                self.showProcessingIndicator()

            case .sessionNotRunning:
                self.shutterButton.isEnabled = false

            @unknown default:
                break
            }
        }
    }
}

为何使用就绪状态协调器:没有它,你就需要手动跟踪拍摄状态,而且用户可能会在处理期间反复点击快门按钮。

画质优先级(基准方案)

即使没有新 API,这项功能仍然有用:

func capturePhoto() {
    var settings = AVCapturePhotoSettings()

    // Speed vs Quality tradeoff
    // .speed     - Fastest capture, lower quality
    // .balanced  - Good default
    // .quality   - Best quality, may have delay
    settings.photoQualityPrioritization = .speed

    // For specific use cases:
    // - Social sharing: .speed (users expect instant)
    // - Document scanning: .quality (accuracy matters)
    // - General photography: .balanced

    photoOutput.capturePhoto(with: settings, delegate: self)
}

延迟处理(iOS 17+):

为了获得最高响应速度,拍摄会立即返回代理图像,完整的 Deep Fusion 处理则在后台进行:

// Check support and enable deferred processing
if photoOutput.isAutoDeferredPhotoDeliverySupported {
    photoOutput.isAutoDeferredPhotoDeliveryEnabled = true
}

使用延迟处理时的代理回调:

// Called for BOTH regular photos AND deferred proxies
func photoOutput(_ output: AVCapturePhotoOutput,
                 didFinishProcessingPhoto photo: AVCapturePhoto,
                 error: Error?) {
    guard error == nil else { return }

    // Non-deferred photo - save directly
    if !photo.isRawPhoto, let data = photo.fileDataRepresentation() {
        savePhotoToLibrary(data)
    }
}

// Called ONLY for deferred proxies - save to PhotoKit for later processing
func photoOutput(_ output: AVCapturePhotoOutput,
                 didFinishCapturingDeferredPhotoProxy deferredPhotoProxy: AVCaptureDeferredPhotoProxy,
                 error: Error?) {
    guard error == nil else { return }

    // CRITICAL: Save proxy to library ASAP before app is backgrounded
    // App may be force-quit if memory pressure is high during backgrounding
    guard let proxyData = deferredPhotoProxy.fileDataRepresentation() else { return }

    Task {
        try await PHPhotoLibrary.shared().performChanges {
            let request = PHAssetCreationRequest.forAsset()
            // Use .photoProxy resource type - triggers deferred processing in Photos
            request.addResource(with: .photoProxy, data: proxyData, options: nil)
        }
    }
}

最终处理的时机:

  • 从 PhotoKit 请求图像时按需处理
  • 或在设备空闲时(已接入电源且未使用)自动处理

了解延迟处理状态后获取图像:

// Request with secondary degraded image for smoother UX
let options = PHImageRequestOptions()
options.allowSecondaryDegradedImage = true  // New in iOS 17

PHImageManager.default().requestImage(
    for: asset,
    targetSize: targetSize,
    contentMode: .aspectFill,
    options: options
) { image, info in
    let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool ?? false

    if isDegraded {
        // First: Low quality (immediate)
        // Second: Medium quality (new - while processing)
        // Third callback will be final quality
        self.showTemporaryImage(image)
    } else {
        // Final quality - processing complete
        self.showFinalImage(image)
    }
}

要求:iPhone 11 Pro 及更新机型。不用于闪光灯拍摄,也不用于无法从扩展处理中获益的格式。

重要注意事项:

  • 无法对延迟处理的照片应用像素缓冲区自定义操作(滤镜、元数据更改)
  • 处理完成后,使用 PhotoKit 调整照片
  • 尽快将代理图像保存到图库——应用在后台运行时,可用时间有限

成本:1 小时实现时间,可避免用户抱怨“相机感觉很慢”

模式 5:处理会话中断

使用场景:处理来电、多任务处理和系统相机使用等情况。

class CameraManager {
    private var interruptionObservers: [NSObjectProtocol] = []

    func setupInterruptionHandling() {
        // Session was interrupted
        let interruptedObserver = NotificationCenter.default.addObserver(
            forName: .AVCaptureSessionWasInterrupted,
            object: session,
            queue: .main
        ) { [weak self] notification in
            guard let reason = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int,
                  let interruptionReason = AVCaptureSession.InterruptionReason(rawValue: reason) else {
                return
            }

            switch interruptionReason {
            case .videoDeviceNotAvailableInBackground:
                // App went to background - normal, will resume
                self?.showPausedOverlay()

            case .audioDeviceInUseByAnotherClient:
                // Another app using audio
                self?.showInterruptedBanner("Audio in use by another app")

            case .videoDeviceInUseByAnotherClient:
                // Another app using camera
                self?.showInterruptedBanner("Camera in use by another app")

            case .videoDeviceNotAvailableWithMultipleForegroundApps:
                // Split View/Slide Over - camera not available
                self?.showInterruptedBanner("Camera unavailable in Split View")

            case .videoDeviceNotAvailableDueToSystemPressure:
                // Thermal state - reduce quality or stop
                self?.handleThermalPressure()

            @unknown default:
                self?.showInterruptedBanner("Camera interrupted")
            }
        }
        interruptionObservers.append(interruptedObserver)

        // Session interruption ended
        let endedObserver = NotificationCenter.default.addObserver(
            forName: .AVCaptureSessionInterruptionEnded,
            object: session,
            queue: .main
        ) { [weak self] _ in
            self?.hideInterruptedBanner()
            self?.hidePausedOverlay()
            // Session automatically resumes - no need to call startRunning()
        }
        interruptionObservers.append(endedObserver)
    }

    deinit {
        interruptionObservers.forEach { NotificationCenter.default.removeObserver($0) }
    }
}

成本:30 分钟实现时间,可避免收到“相机卡死”的错误报告

模式 6:切换相机(前置/后置)

使用场景:在前置和后置相机之间切换。

func switchCamera() {
    sessionQueue.async { [self] in
        guard let currentInput = session.inputs.first as? AVCaptureDeviceInput else {
            return
        }

        let currentPosition = currentInput.device.position
        let newPosition: AVCaptureDevice.Position = currentPosition == .back ? .front : .back

        guard let newDevice = AVCaptureDevice.default(
            .builtInWideAngleCamera,
            for: .video,
            position: newPosition
        ) else {
            return
        }

        session.beginConfiguration()
        defer { session.commitConfiguration() }

        // Remove old input
        session.removeInput(currentInput)

        // Add new input
        do {
            let newInput = try AVCaptureDeviceInput(device: newDevice)
            if session.canAddInput(newInput) {
                session.addInput(newInput)

                // Update rotation coordinator for new device
                if let previewLayer = previewLayer {
                    setupRotationCoordinator(device: newDevice, previewLayer: previewLayer)
                }
            } else {
                // Fallback: restore old input
                session.addInput(currentInput)
            }
        } catch {
            session.addInput(currentInput)
        }
    }
}

前置相机镜像:默认情况下,前置相机预览会镜像显示(符合用户预期)。拍摄的照片不会镜像(便于正确分享)。这是有意为之。

成本:20 分钟实现时间

模式 7:视频录制

使用场景:录制带音频的视频并保存到文件。

class CameraManager: NSObject {
    let movieOutput = AVCaptureMovieFileOutput()
    private var currentRecordingURL: URL?

    func setupVideoRecording() {
        sessionQueue.async { [self] in
            session.beginConfiguration()
            defer { session.commitConfiguration() }

            // Set video preset
            session.sessionPreset = .high  // Or .hd1920x1080, .hd4K3840x2160

            // Add microphone input
            if let microphone = AVCaptureDevice.default(for: .audio),
               let audioInput = try? AVCaptureDeviceInput(device: microphone),
               session.canAddInput(audioInput) {
                session.addInput(audioInput)
            }

            // Add movie output
            if session.canAddOutput(movieOutput) {
                session.addOutput(movieOutput)
            }
        }
    }

    func startRecording() {
        guard !movieOutput.isRecording else { return }

        let outputURL = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
            .appendingPathExtension("mov")

        currentRecordingURL = outputURL

        // Apply rotation
        if let connection = movieOutput.connection(with: .video) {
            connection.videoRotationAngle = captureRotationAngle()
        }

        movieOutput.startRecording(to: outputURL, recordingDelegate: self)
    }

    func stopRecording() {
        guard movieOutput.isRecording else { return }
        movieOutput.stopRecording()
    }
}

extension CameraManager: AVCaptureFileOutputRecordingDelegate {
    func fileOutput(_ output: AVCaptureFileOutput,
                    didFinishRecordingTo outputFileURL: URL,
                    from connections: [AVCaptureConnection],
                    error: Error?) {
        if let error = error {
            print("Recording error: \(error)")
            return
        }

        // Video saved to outputFileURL
        saveVideoToPhotoLibrary(outputFileURL)
    }
}

成本:45 分钟实现时间

反模式

反模式 1:在主线程上执行会话操作

错误:

func startCamera() {
    session.startRunning()  // Blocks UI for 1-3 seconds!
}

正确:

func startCamera() {
    sessionQueue.async { [self] in
        session.startRunning()
    }
}

原因:startRunning() 会阻塞。在主线程上执行时,界面会卡住。

反模式 2:使用已弃用的 videoOrientation

错误(iOS 17 之前):

// Manually tracking orientation
NotificationCenter.default.addObserver(
    forName: UIDevice.orientationDidChangeNotification,
    object: nil,
    queue: .main
) { _ in
    // Manual rotation logic...
}

正确(iOS 17+):

let coordinator = AVCaptureDevice.RotationCoordinator(device: camera, previewLayer: preview)
// Automatically tracks gravity, provides angles

原因:RotationCoordinator 能处理手动跟踪容易遗漏的边缘情况(屏幕朝上、屏幕朝下)。

反模式 3:忽略会话中断

错误:

// No interruption handling - camera freezes on phone call

正确:

NotificationCenter.default.addObserver(
    forName: .AVCaptureSessionWasInterrupted,
    object: session,
    queue: .main
) { notification in
    // Show UI feedback
}

原因:不处理中断时,相机在发生中断后看起来会卡住。

反模式 4:不使用配置代码块就修改会话

错误:

session.removeInput(oldInput)
session.addInput(newInput)  // May fail mid-stream

正确:

session.beginConfiguration()
session.removeInput(oldInput)
session.addInput(newInput)
session.commitConfiguration()  // Atomic change

原因:不使用配置代码块时,会话可能会在两次调用之间进入无效状态。

压力场景

场景 1:“周五前让相机能用就行”

背景:产品团队希望发布相机功能。你正在考虑跳过中断处理。

压力:“我测试时能用,发布吧。”

现实:第一个使用相机时接到电话的用户就会看到界面卡住。App Store 审核也可能发现这个问题。

正确做法:

  1. 实现中断处理(30 分钟)
  2. 在使用相机时拨打测试设备的电话进行测试
  3. 确认界面会显示适当的反馈

回应模板:“相机拍摄功能正常,但有来电时应用会卡住。我需要 30 分钟妥善处理中断,避免收到 1 星评价。”

场景 2:“相机太慢了”

背景:QA 反馈拍照感觉迟缓。PM 希望它“像系统相机一样即时”。

压力:“想办法让它快点。”

实际情况:默认设置优先保证画质而非速度。系统相机使用延迟处理。

正确做法:

  1. 对社交分享场景,将 photoQualityPrioritization = .speed 设为优先速度
  2. 考虑使用延迟处理,以获得最快响应
  3. 立即显示拍摄动画(无需等待处理完成)

回应模板:“我们目前优先优化图像质量。我可以通过优先保证速度,并在后台继续处理的同时立即显示预览,让拍摄看起来即时完成。系统相机 App 采用的就是这种方式。”

场景 3:“为什么前置相机拍出的照片是镜像的?”

背景:设计师反馈前置相机拍出的照片看起来“不对”——照片没有像预览那样镜像显示。

压力:“预览是什么样,照片就应该是什么样。”

实际情况:预览是镜像的(符合用户对镜子的预期)。照片不是镜像的(便于分享,文字方向正确)。这是有意设计的行为,与系统相机一致。

正确做法:

  1. 说明这是 Apple 的标准行为
  2. 如果业务需要镜像照片(如自拍 App),可在后期处理中手动镜像
  3. 预览的镜像方式始终应符合用户预期

回应模板:“这是 Apple 有意设计的行为。预览会像镜子一样镜像显示,方便用户取景;但拍摄的照片不会镜像,这样分享时文字方向才是正确的。如果我们的使用场景需要,也可以在后期处理中添加可选的镜像功能。”

检查清单

发布相机功能前:

会话设置:

  • ☑ 所有会话操作均在专用串行队列上执行
  • ☑ startRunning() 绝不在主线程上调用
  • ☑ 会话预设与使用场景匹配(拍照使用 .photo,视频使用 .high)
  • ☑ 配置更改均由 beginConfiguration()/commitConfiguration() 包裹

权限:

  • ☑ 在设置会话前请求相机权限
  • ☑ 在 Info.plist 中添加 NSCameraUsageDescription
  • ☑ 如果录制音频,则添加 NSMicrophoneUsageDescription
  • ☑ 妥善处理权限被拒绝的情况

旋转:

  • ☑ 使用 RotationCoordinator(而非已弃用的 videoOrientation)
  • ☑ 通过观察机制更新预览图层的旋转
  • ☑ 拍照时应用拍摄旋转角度
  • ☑ 在所有方向下测试(纵向、横向、屏幕朝上)

响应速度:

  • ☑ 根据使用场景适当设置 photoQualityPrioritization
  • ☑ 拍摄按钮立即提供反馈
  • ☑ 考虑使用延迟处理以获得最快速度

中断:

  • ☑ 注册会话中断观察器
  • ☑ 中断时显示 UI 反馈
  • ☑ 使用来电进行测试
  • ☑ 在分屏模式下测试(iPad)

切换相机:

  • ☑ 切换前置/后置相机时更新旋转协调器
  • ☑ 在会话队列上执行切换
  • ☑ 新相机不可用时提供回退方案

视频录制(如适用):

  • ☑ 添加麦克风输入
  • ☑ 录制代理处理完成事件
  • ☑ 清理临时录制文件

资源

WWDC:2021-10247、2023-10105

文档:/avfoundation/avcapturesession、/avfoundation/avcapturedevice/rotationcoordinator、/avfoundation/avcapturephotosettings、/avfoundation/avcapturephotooutputreadinesscoordinator

Skills:axiom-camera-capture-ref、axiom-camera-capture-diag、axiom-photo-library

评论

支持 Markdown