SwiftUI中AVCaptureSession运行时VStack/ScrollView内视图无响应
问题:CameraView运行时ScrollView内其他视图无触摸响应
我的布局是ScrollView内包含文本、按钮和CameraView组件。当CameraView运行(显示摄像头画面)时,其他视图无法响应触摸操作;关闭CameraView后恢复正常。已排查性能无卡顿,相关代码如下:
视图代码
VStack { ZStack{ ScrollView{ HStack(alignment: .top) { LogoMainView() } .frame(maxWidth: .infinity, maxHeight: 44, alignment: .leading) .padding(.horizontal,5) VStack{ HeaderText(title:"Some header") ParagraphText(text:"Some random text") CameraDropDown( toggleFront:{ self.captureSession.changeOrientation(orientation: .front) },toggleBack: { self.captureSession.changeOrientation(orientation: .back) }) CameraView() .onAppear(){ self.appDelegate.captureSession.setup() self.appDelegate.captureSession.start() } .onDisappear(){ self.appDelegate.captureSession.stop() } .onChange(of: self.appDelegate.captureSession.orientation){ newvalue in self.appDelegate.captureSession.orientation = newvalue self.appDelegate.captureSession.setup() self.appDelegate.captureSession.start() } } .padding(.horizontal,10) } } } .clipped()
摄像头类代码
// // CaptureSession.swift import Foundation import AVFoundation import SwiftUI class CaptureSession: NSObject, ObservableObject { @Published var sampleBuffer: CMSampleBuffer? @Published var orientation: AVCaptureDevice.Position = .back var captureSession: AVCaptureSession? func setup() { var allowedAccess = false let blocker = DispatchGroup() blocker.enter() AVCaptureDevice.requestAccess(for: .video) { flag in allowedAccess = flag blocker.leave() } blocker.wait() if !allowedAccess { return } if !allowedAccess { print("Camera access is not allowed.") return } let session = AVCaptureSession() session.beginConfiguration() let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: orientation) guard videoDevice != nil, let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!), session.canAddInput(videoDeviceInput) else { print("Unable to detect camera.") return } session.addInput(videoDeviceInput) session.commitConfiguration() let videoOutput = AVCaptureVideoDataOutput() videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "SampleBuffer")) if (session.canAddOutput(videoOutput)) { session.addOutput(videoOutput) } self.captureSession = session } func start() { guard let captureSession = self.captureSession else { return } if (!captureSession.isRunning) { DispatchQueue.global(qos: .default).async { captureSession.startRunning() } } } func stop() { guard let captureSession = self.captureSession else { return } if (captureSession.isRunning) { captureSession.stopRunning() } } func changeOrientation(orientation:AVCaptureDevice.Position) { guard let captureSession = self.captureSession else { return } if(captureSession.isRunning){ DispatchQueue.main.async { self.orientation = orientation self.setup() self.start() } }else{ DispatchQueue.main.async { self.orientation = orientation self.setup() self.start() } } } } extension CaptureSession: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { DispatchQueue.main.async { self.sampleBuffer = sampleBuffer } } }
核心问题分析
- 主线程阻塞:
setup()方法中使用blocker.wait()同步等待相机权限请求,直接卡住主线程,导致UI无法响应任何触摸事件——这是最关键的原因。 - 重复创建会话:每次切换摄像头都重新调用
setup()创建新的AVCaptureSession,效率低下且易引发线程冲突。 - CameraView触摸拦截:默认情况下,CameraView(通常是UIViewRepresentable包装的AVCapturePreviewLayer)会拦截触摸事件,阻止下层视图接收交互。
修复步骤与代码修改
1. 修复CaptureSession的主线程阻塞问题
移除同步等待的DispatchGroup,改用异步回调处理权限请求,同时复用已有会话:
func setup(completion: (() -> Void)? = nil) { AVCaptureDevice.requestAccess(for: .video) { [weak self] allowedAccess in guard let self = self else { return } if !allowedAccess { print("Camera access is not allowed.") completion?() return } // 仅在会话未创建时初始化,否则直接更新设备 if self.captureSession == nil { let session = AVCaptureSession() session.beginConfiguration() guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: self.orientation), let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice), session.canAddInput(videoDeviceInput) else { print("Unable to detect camera.") completion?() return } session.addInput(videoDeviceInput) let videoOutput = AVCaptureVideoDataOutput() videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "SampleBuffer")) if session.canAddOutput(videoOutput) { session.addOutput(videoOutput) } session.commitConfiguration() self.captureSession = session } else { // 已有会话,仅切换输入设备 self.switchCameraDevice() } completion?() } } // 添加单独的摄像头切换方法 private func switchCameraDevice() { guard let captureSession = self.captureSession else { return } captureSession.beginConfiguration() // 移除旧的输入设备 if let currentInput = captureSession.inputs.first as? AVCaptureDeviceInput { captureSession.removeInput(currentInput) } // 添加新的输入设备 guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: self.orientation), let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice), captureSession.canAddInput(videoDeviceInput) else { print("Failed to switch camera device.") captureSession.commitConfiguration() return } captureSession.addInput(videoDeviceInput) captureSession.commitConfiguration() }
2. 优化changeOrientation方法
避免重复创建会话,直接调用切换设备方法:
func changeOrientation(orientation: AVCaptureDevice.Position) { guard orientation != self.orientation else { return } // 避免重复切换 self.orientation = orientation if let captureSession = self.captureSession { if captureSession.isRunning { captureSession.stopRunning() self.switchCameraDevice() DispatchQueue.global(qos: .default).async { captureSession.startRunning() } } else { self.switchCameraDevice() } } else { // 会话未初始化,调用setup self.setup { [weak self] in self?.start() } } }
3. 修复视图中的回调逻辑
移除onChange中重复的setup调用,并且禁用CameraView的触摸响应:
CameraView() .allowsHitTesting(false) // 禁用CameraView的触摸响应,让下层视图接收事件 .onAppear(){ self.appDelegate.captureSession.setup { self.appDelegate.captureSession.start() } } .onDisappear(){ self.appDelegate.captureSession.stop() } // 移除原有的onChange,因为changeOrientation已经处理了方向切换
关键说明
- 主线程不阻塞:权限请求改为异步处理,确保UI线程始终能响应触摸事件。
- 会话复用:不再每次切换摄像头都重建会话,而是直接替换输入设备,提升性能和稳定性。
- 触摸穿透:通过
allowsHitTesting(false)让CameraView的触摸事件穿透到下层的ScrollView和按钮,确保交互正常。
内容的提问来源于stack exchange,提问作者EMT
相关产品推荐
相关产品推荐

