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

自定义相机拍照时崩溃:Thread 1遇Delegate nil解包错误

问题描述

已成功实现带有实时预览功能的自定义相机,但尝试拍照时出现以下错误:

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

错误发生在CameraService的capturePhoto方法中强制解包delegate!的位置。

以下是相关代码:

CameraService.swift

class CameraService {
var session: AVCaptureSession?
var delegate: AVCapturePhotoCaptureDelegate?


let output = AVCapturePhotoOutput()
let previewLayer = AVCaptureVideoPreviewLayer()

func start(delegate: AVCapturePhotoCaptureDelegate, completion:@escaping (Error?) -> ()) {
    self.delegate = delegate
    self.checkPermissions(completion: completion)
}

private func checkPermissions(completion:@escaping (Error?) -> ()){
    switch AVCaptureDevice.authorizationStatus(for: .video){
            
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                guard granted else { return }
                
                DispatchQueue.main.async {
                    self?.setupCamera(completion: completion)
                }
            }
        case .restricted:
            break
        case .denied:
            break
        case .authorized:
            setupCamera(completion: completion)
        @unknown default:
            break
    }
}

private func setupCamera(completion:@escaping (Error?) -> ()) {
    let session = AVCaptureSession()
    if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front){
        do {
            let input = try AVCaptureDeviceInput(device: device)
            if session.canAddInput(input) {
                session.addInput(input)
            }
            
            if session.canAddOutput(output) {
                session.addOutput(output)
            }
            previewLayer.videoGravity = .resizeAspectFill
            
            previewLayer.session = session
            
            DispatchQueue.global(qos:.userInitiated).async {
                session.startRunning()
            }
          
            self.session = session
            
        } catch {
            completion(error)
        }
    }
}

func capturePhoto(with settings: AVCapturePhotoSettings = AVCapturePhotoSettings()) {
    
    output.capturePhoto(with: settings, delegate: delegate!)  // 错误发生处
}
  }

CameraView.swift

struct CameraView: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIViewController
    
    let cameraService: CameraService
    let didFinishProcessingPhoto: (Result<AVCapturePhoto, Error>) -> ()
    
    func makeUIViewController(context: Context) -> UIViewController {
        
        cameraService.start(delegate: context.coordinator) { err in
            if let err = err {
                didFinishProcessingPhoto(.failure(err))
                return
            }
        }
        
        let viewController = UIViewController()
        viewController.view.backgroundColor = .black
        viewController.view.layer.addSublayer(cameraService.previewLayer)
        cameraService.previewLayer.frame = viewController.view.bounds
        return viewController
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self, didFinishProcessingPhoto: didFinishProcessingPhoto)
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
    
    class Coordinator: NSObject, AVCapturePhotoCaptureDelegate {
        let parent: CameraView
        private var didFinishProcessingPhoto: (Result<AVCapturePhoto, Error>) -> ()
        
        init(_ parent: CameraView,
             didFinishProcessingPhoto: @escaping (Result<AVCapturePhoto, Error>) -> ()) {
            self.parent = parent
            self.didFinishProcessingPhoto = didFinishProcessingPhoto
        }
        
        func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
            if let error = error {
                didFinishProcessingPhoto(.failure(error))
                return
            }
            didFinishProcessingPhoto(.success(photo))
        }
    }
}

CustomCameraView.swift

import SwiftUI
import AVFoundation


struct CustomCameraView: View {
    let cameraService = CameraService()
    @Binding var capturedImage: UIImage?
    
    @Environment(\.presentationMode) private var presentationMode
    var body: some View {
        ZStack {
            CameraView(cameraService: cameraService, didFinishProcessingPhoto: { result in
                switch result {
                    case .success(let photo):
                        if let data = photo.fileDataRepresentation() {
                            capturedImage = UIImage(data: data)
                        } else {
                            print("Error: no image data found.")
                        }
                    case .failure(let err):
                        print(err.localizedDescription)
                }
            })
                .frame(maxWidth:.infinity, maxHeight: .infinity)
                .ignoresSafeArea()
            VStack{
                Button {
                    cameraService.capturePhoto()
                } label: {
                    Circle()
                        .fill(.white)
                        .frame(width: 60, height:60)
                }
            }
        }
    }
}

解决方案

核心问题分析

崩溃的直接原因是CameraService中的delegate属性为nil,但你在capturePhoto中使用了强制解包delegate!。可能的触发场景包括:

  1. 用户在相机权限请求尚未完成时就点击拍照按钮
  2. 权限被拒绝/受限后,start方法未正确处理,但用户仍能点击拍照
  3. 极端情况下Coordinator被提前释放导致delegate变为nil

修复步骤

1. 安全解包delegate并添加前置检查

修改CameraService的capturePhoto方法,避免强制解包,同时检查输出是否正常:

func capturePhoto(with settings: AVCapturePhotoSettings = AVCapturePhotoSettings()) {
    // 检查delegate是否存在
    guard let captureDelegate = self.delegate else {
        print("错误:拍照代理未设置,无法执行拍照操作")
        return
    }
    // 检查照片输出是否已连接到视频会话
    guard output.connection(with: .video) != nil else {
        print("错误:照片输出未与视频会话建立连接")
        return
    }
    output.capturePhoto(with: settings, delegate: captureDelegate)
}

2. 完善权限处理逻辑

在checkPermissions中补充权限拒绝/受限的错误回调,让上层可以提示用户:

private func checkPermissions(completion:@escaping (Error?) -> ()){
    switch AVCaptureDevice.authorizationStatus(for: .video){
            
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    if granted {
                        self.setupCamera(completion: completion)
                    } else {
                        completion(NSError(domain: "CameraPermission", code: -1, userInfo: [NSLocalizedDescriptionKey: "相机权限已被拒绝"]))
                    }
                }
            }
        case .restricted:
            completion(NSError(domain: "CameraPermission", code: -2, userInfo: [NSLocalizedDescriptionKey: "相机权限受限制"]))
        case .denied:
            completion(NSError(domain: "CameraPermission", code: -3, userInfo: [NSLocalizedDescriptionKey: "相机权限已被拒绝"]))
        case .authorized:
            setupCamera(completion: completion)
        @unknown default:
            completion(NSError(domain: "CameraPermission", code: -4, userInfo: [NSLocalizedDescriptionKey: "未知的相机权限状态"]))
    }
}

3. 完善相机初始化的错误处理

在setupCamera中补充设备不存在、无法添加输入/输出的错误回调:

private func setupCamera(completion:@escaping (Error?) -> ()) {
    let session = AVCaptureSession()
    // 检查是否有可用的前置摄像头
    guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
        completion(NSError(domain: "CameraSetup", code: -1, userInfo: [NSLocalizedDescriptionKey: "无可用的前置摄像头"]))
        return
    }
    do {
        let input = try AVCaptureDeviceInput(device: device)
        guard session.canAddInput(input) else {
            completion(NSError(domain: "CameraSetup", code: -2, userInfo: [NSLocalizedDescriptionKey: "无法将相机输入添加到会话"]))
            return
        }
        session.addInput(input)
        
        guard session.canAddOutput(output) else {
            completion(NSError(domain: "CameraSetup", code: -3, userInfo: [NSLocalizedDescriptionKey: "无法将照片输出添加到会话"]))
            return
        }
        session.addOutput(output)
        
        previewLayer.videoGravity = .resizeAspectFill
        previewLayer.session = session
        
        DispatchQueue.global(qos:.userInitiated).async {
            session.startRunning()
        }
      
        self.session = session
        completion(nil)
        
    } catch {
        completion(error)
    }
}

4. 上层添加错误提示(可选)

在CustomCameraView的错误回调中添加用户提示,比如用弹窗告知权限问题:

CameraView(cameraService: cameraService, didFinishProcessingPhoto: { result in
    switch result {
        case .success(let photo):
            if let data = photo.fileDataRepresentation() {
                capturedImage = UIImage(data: data)
            } else {
                print("Error: no image data found.")
            }
        case .failure(let err):
            print(err.localizedDescription)
            // 这里可以添加UIAlertController或SwiftUI的弹窗提示用户
    }
})

内容的提问来源于stack exchange,提问作者Luka Šalipur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 14:39:59