SwiftUI 自定义相机实时预览持续崩溃问题排查
崩溃根因
该问题和SwiftUI本身无关,由配置缺失、不规范写法共同导致:
- 缺失相机权限声明:iOS 系统强制要求调用摄像头前,在
Info.plist中添加NSCameraUsageDescription键。缺项时触发相机流程会被系统直接杀死应用,不会弹出授权提示。 - 空值强制解包:
AVCaptureDevice.default(...)在模拟器运行、摄像头被占用/硬件故障时会返回nil,原代码直接用device!强制解包,立刻触发崩溃。 - 线程违规:相机授权回调默认在后台线程执行,原代码直接在回调中修改
@Published状态、配置会话,违反SwiftUI状态必须在主线程更新的规则,会触发偶发崩溃。
此外即使不崩溃也看不到预览:原代码没有把CameraPreview加入CameraView的视图层级,ZStack中只有黑色背景和操作按钮。
修复步骤
1. 添加权限声明
打开项目对应Target的「Info」标签页,在自定义iOS目标属性中添加一行:
- 键名:
Privacy - Camera Usage Description - 内容:填写面向用户的相机用途说明,例如「需要访问相机完成拍摄」
2. 修复相机模型
移除不必要的隐式解包预览层属性,替换强制解包为安全判空,所有会话操作放在独立串行队列,避免阻塞主线程:
class CameraModel: ObservableObject { @Published var isTaken = false @Published var session = AVCaptureSession() @Published var alert = false @Published var output = AVCapturePhotoOutput() // 相机会话专用调度队列,避免阻塞UI线程 private let sessionQueue = DispatchQueue(label: "camera.session.queue") func check() { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: setUp() case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in // 回调默认在后台,UI操作必须切回主线程 DispatchQueue.main.async { guard granted else { self?.alert = true return } self?.setUp() } } case .denied, .restricted: alert = true @unknown default: break } } func setUp() { sessionQueue.async { [weak self] in guard let self = self else { return } self.session.beginConfiguration() // 安全获取摄像头,不做强制解包 guard let device = AVCaptureDevice.default( .builtInWideAngleCamera, for: .video, position: .front ) else { self.session.commitConfiguration() print("无法获取摄像头设备,请使用真机调试") return } do { let input = try AVCaptureDeviceInput(device: device) if self.session.canAddInput(input) { self.session.addInput(input) } if self.session.canAddOutput(self.output) { self.session.addOutput(self.output) } self.session.commitConfiguration() // 配置完成后再启动会话 if !self.session.isRunning { self.session.startRunning() } } catch { // 配置失败回滚,避免会话处于异常状态 self.session.rollbackConfiguration() print("相机配置失败:\(error.localizedDescription)") } } } // 视图消失时停止会话,降低电量消耗 func stopSession() { sessionQueue.async { [weak self] in if self?.session.isRunning == true { self?.session.stopRunning() } } } }
3. 修复预览视图
通过重写layerClass让预览层自动匹配父视图尺寸,不要写死屏幕宽高,同时不需要将预览层声明为可观察状态:
struct CameraPreview: UIViewRepresentable { let session: AVCaptureSession func makeUIView(context: Context) -> PreviewContainerView { let view = PreviewContainerView() view.previewLayer.session = session view.previewLayer.videoGravity = .resizeAspectFill return view } func updateUIView(_ uiView: PreviewContainerView, context: Context) {} } // 预览层容器,自动适配布局变化 class PreviewContainerView: UIView { override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer } }
4. 将预览加入视图层级
把预览放在ZStack最底层,补充权限弹窗和会话生命周期管理,保留原有业务逻辑:
struct CameraView: View { @Environment(\.presentationMode) var presentationMode @StateObject var camera = CameraModel() var body: some View { ZStack { // 相机预览放在最底层 CameraPreview(session: camera.session) .edgesIgnoringSafeArea(.all) VStack { Button(action: { presentationMode.wrappedValue.dismiss() }, label: { Image(systemName: "multiply") .font(.system(size: 40, weight: .semibold)) .foregroundColor(Color.white.opacity(0.9)) .shadow(color: Color.black.opacity(0.6), radius: 1, x: 0, y: 2) }) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.trailing, 30) Spacer() HStack { Button(action: {}) { Image(systemName: "arrow.triangle.2.circlepath.camera") .font(.largeTitle) .foregroundColor(.white) } .padding(.leading) Spacer() if camera.isTaken { Spacer() Button(action: { camera.isTaken.toggle() }, label: { ZStack { Circle() .fill(Color.red) .frame(width: 65, height: 65) Circle() .stroke(Color.white, lineWidth: 2) .frame(width: 75, height: 75) } }) Spacer() } else { Button(action: { camera.isTaken.toggle() }, label: { ZStack { Circle() .fill(Color.white) .frame(width: 65, height: 65) Circle() .stroke(Color.white, lineWidth: 2) .frame(width: 75, height: 75) } }) } Spacer() Button(action: {}) { Image(systemName: "bolt.circle") .font(.largeTitle) .foregroundColor(.white) } .padding(.trailing) } .frame(height: 75) } } .onAppear(perform: { camera.check() }) .onDisappear { camera.stopSession() } .alert("需要相机权限", isPresented: $camera.alert) { Button("去设置") { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url) } Button("取消", role: .cancel) {} } message: { Text("请在系统设置中开启相机权限后重试") } } }
注意事项
- 相机功能必须在真机上调试,模拟器无对应硬件,仅能验证流程不崩溃,无法输出实时画面。
- 后续如果要录制带声音的视频,需要额外添加
NSMicrophoneUsageDescription权限声明。 - 原代码中切换摄像头、闪光灯按钮为空实现,不影响预览功能,后续补充对应逻辑即可。
内容的提问来源于stack exchange,提问作者Joshua Gibson-Bascombe
相关产品推荐
相关产品推荐

