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

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
        }
    }
}

核心问题分析

  1. 主线程阻塞:setup()方法中使用blocker.wait()同步等待相机权限请求,直接卡住主线程,导致UI无法响应任何触摸事件——这是最关键的原因。
  2. 重复创建会话:每次切换摄像头都重新调用setup()创建新的AVCaptureSession,效率低下且易引发线程冲突。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 04:25:32