You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS相机应用问题求助:启动慢、首次拍照失败等

SwiftUI相机应用问题解决方案(重点解决首次拍照失败)

核心问题2:首次拍照失败(hasFigCaptureSession断言错误)

问题根源

  1. 未请求相机权限:AVFoundation要求必须先获取相机权限才能正常使用,原代码完全跳过权限申请流程,首次启动时系统权限未授权,导致会话初始化不完整。
  2. 会话未完全启动就触发拍照:captureSession.startRunning()是异步执行的,首次点击按钮时会话可能还处于启动过程中,此时调用capturePhoto会因会话未就绪触发断言错误。
  3. 不安全的输出添加逻辑:直接强制解包photoOutput!存在风险,若会话无法添加输出,后续拍照必然失败。

修复方案

  1. 添加相机权限请求:在会话初始化前先检查并请求相机权限,确保权限通过后再配置会话。
  2. 会话就绪状态跟踪:添加状态变量标记会话是否完全启动,只有就绪后才允许触发拍照。
  3. 安全的会话配置逻辑:优化输入输出的添加判断,避免强制解包操作。

问题1:首次启动按钮响应延迟

问题根源

首次启动时未处理权限请求,会话初始化逻辑在主线程存在隐式阻塞,加上权限弹窗的延迟,导致按钮响应卡顿。

修复方案

配合权限请求流程,将会话初始化逻辑放在权限通过后执行,同时确保所有耗时操作在后台线程执行,主线程仅处理UI状态更新。


问题3:快门音量逐渐增大

问题根源

每次拍照都创建新的AVCapturePhotoSettings,且未及时清理回调闭包,可能导致系统重复触发快门音频播放逻辑。

修复方案

复用AVCapturePhotoSettings实例(参数无需变更时),同时在拍照完成后及时清理回调闭包,避免重复触发。


修改后的完整代码

import SwiftUI
import AVFoundation

struct ContentView: View {
    @State private var averageGreen: Double = 0.0
    @State private var isCameraReady = false
    
    var body: some View {
        VStack {
            Text("Average Green Value: \(averageGreen, specifier: "%.2f")")
                .padding()

            Button(action: {
                CameraController.shared.capturePhoto { photoData in
                    guard let photoData = photoData, let uiImage = UIImage(data: photoData) else { return }
                    self.averageGreen = calculateAverageGreenValue(image: uiImage)
                }
            }) {
                Text(isCameraReady ? "Take Photo" : "Preparing Camera...")
                    .padding()
                    .background(isCameraReady ? Color.blue : Color.gray)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            .disabled(!isCameraReady)
        }
        .padding()
        .onAppear {
            CameraController.shared.requestCameraPermission { granted in
                DispatchQueue.main.async {
                    self.isCameraReady = granted
                }
            }
        }
    }
}

func calculateAverageGreenValue(image: UIImage) -> Double {
    guard let cgImage = image.cgImage else { return 0.0 }
    guard let data = cgImage.dataProvider?.data else { return 0.0 }
    guard let ptr = CFDataGetBytePtr(data) else { return 0.0 }

    let width = cgImage.width
    let height = cgImage.height
    let bytesPerPixel = 4

    var totalGreen = 0

    for y in 0..<height {
        for x in 0..<width {
            let pixelIndex = ((width * y) + x) * bytesPerPixel
            let g = Int(ptr[pixelIndex + 1]) // Green component
            totalGreen += g
        }
    }

    return Double(totalGreen) / Double(width * height)
}

class CameraController: NSObject, AVCapturePhotoCaptureDelegate {
    static let shared = CameraController()
    
    private var captureSession: AVCaptureSession?
    private var photoOutput: AVCapturePhotoOutput?
    private var completionHandler: ((Data?) -> Void)?
    // 会话就绪状态标记
    private var isSessionRunning = false

    override init() {
        super.init()
    }
    
    /// 请求相机权限并初始化会话
    func requestCameraPermission(completion: @escaping (Bool) -> Void) {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            setupSession()
            completion(true)
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                if granted {
                    self?.setupSession()
                }
                completion(granted)
            }
        default:
            completion(false)
        }
    }

    private func setupSession() {
        captureSession = AVCaptureSession()
        captureSession?.sessionPreset = .low

        guard let backCamera = AVCaptureDevice.default(for: .video),
              let input = try? AVCaptureDeviceInput(device: backCamera) else { return }

        guard let captureSession = captureSession else { return }
        
        if captureSession.canAddInput(input) {
            captureSession.addInput(input)
        }

        photoOutput = AVCapturePhotoOutput()
        if let photoOutput = photoOutput, captureSession.canAddOutput(photoOutput) {
            captureSession.addOutput(photoOutput)
        }

        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            self?.captureSession?.startRunning()
            self?.isSessionRunning = true
        }
    }

    func capturePhoto(completion: @escaping (Data?) -> Void) {
        // 会话未就绪时直接返回失败
        guard isSessionRunning, let photoOutput = photoOutput else {
            completion(nil)
            return
        }
        self.completionHandler = completion
        let settings = AVCapturePhotoSettings()
        settings.flashMode = .off
        photoOutput.capturePhoto(with: settings, delegate: self)
    }

    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        defer {
            // 及时清理回调闭包,避免内存泄漏和重复触发
            completionHandler = nil
        }
        if let error = error {
            print("拍照错误:\(error.localizedDescription)")
            completionHandler?(nil)
            return
        }
        completionHandler?(photo.fileDataRepresentation())
    }
}

关键修改点

  1. 权限请求流程:新增requestCameraPermission方法,在ContentView的onAppear中调用,确保权限通过后才初始化会话。
  2. 会话就绪判断:添加isSessionRunning变量,capturePhoto时先检查会话是否启动完成,避免未就绪时触发拍照。
  3. UI状态联动:ContentView中根据相机就绪状态启用/禁用按钮,引导用户操作时机。
  4. 回调清理:在didFinishProcessingPhoto中使用defer清理completionHandler,避免内存泄漏和重复逻辑触发。

内容的提问来源于stack exchange,提问作者user655870

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 22:54:51