SwiftUI中Sheet视图内CameraView无法显示的问题
解决SwiftUI Sheet中相机预览图层不显示的问题
问题现象
使用.sheet弹出包含条码扫描功能的ScannerView时,相机预览画面无法显示,但iPhone顶部的相机绿点已激活,控制台日志显示权限校验、相机会话启动均无异常。改用fullScreenCover或navigationDestination跳转时,相机预览能正常工作。
相关代码
MainView.swift
@State private var showScanSheet = false var body: some View { NavigationStack { VStack { ... }.sheet(isPresented: $showScanSheet) { ScannerView() } } }
ScannerView.swift
import SwiftUI import AVKit struct ScannerView: View { @State private var isScanning: Bool = false @State private var session: AVCaptureSession = .init() @State private var cameraPermission: Permission = .idle @State private var barcodeOutput: AVCaptureMetadataOutput = .init() @State private var errorMessage: String = "" @State private var showError: Bool = false @Environment(\.openURL) private var openURL @StateObject private var barcodeDelegate = BarcodeScannerDelegate() var body: some View { GeometryReader { let size = $0.size ZStack { CameraView(frameSize: CGSize(width: size.width, height: 200), session: $session).scaleEffect(0.97) RoundedRectangle(cornerRadius: 10, style: .circular) .trim(from: 0.55, to: 0.60) .stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round)) .padding() RoundedRectangle(cornerRadius: 10, style: .circular) .trim(from: 0.55, to: 0.60) .stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round)) .rotationEffect(.init(degrees: 180)) .padding() RoundedRectangle(cornerRadius: 10, style: .circular) .trim(from: 0.40, to: 0.45) .stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round)) .padding() RoundedRectangle(cornerRadius: 10, style: .circular) .trim(from: 0.40, to: 0.45) .stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round)) .rotationEffect(.init(degrees: 180)) .padding() } .frame(width: size.width, height: 200) .frame(maxWidth: .infinity, maxHeight: .infinity) } .onAppear(perform: checkCameraPermission) .alert(errorMessage, isPresented: $showError) { if cameraPermission == .denied { Button("Settings") { let settingsString = UIApplication.openSettingsURLString if let settingsURL = URL(string: settingsString) { openURL(settingsURL) } } Button("Cancel", role: .cancel) {} } } } func checkCameraPermission() { print("Checking camera permission") Task { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: cameraPermission = .approved setupCamera() case .notDetermined, .denied, .restricted: if await AVCaptureDevice.requestAccess(for: .video) { cameraPermission = .approved setupCamera() } else { cameraPermission = .denied presentError("Please provide access to the camera for scanning barcodes.") } default: break } print(cameraPermission) } } func setupCamera() { do { guard let device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera], mediaType: .video, position: .back).devices.first else { presentError("Unknown error.") return } let input = try AVCaptureDeviceInput(device: device) guard session.canAddInput(input), session.canAddOutput(barcodeOutput) else { presentError("Unknown error.") return } session.beginConfiguration() session.addInput(input) session.addOutput(barcodeOutput) barcodeOutput.metadataObjectTypes = [.upce, .ean8, .ean13] barcodeOutput.setMetadataObjectsDelegate(barcodeDelegate, queue: .main) session.commitConfiguration() DispatchQueue.global(qos: .background).async { session.startRunning() } } catch { presentError(error.localizedDescription) } } func presentError(_ message: String) { errorMessage = message showError.toggle() } }
BarcodeScannerDelegate.swift
import Foundation import AVKit class BarcodeScannerDelegate: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate { func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { if let metadataObject = metadataObjects.first { guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } guard let scannedCode = readableObject.stringValue else { return } print(scannedCode) } } }
CameraView.swift
import SwiftUI import AVKit struct CameraView: UIViewRepresentable { var frameSize: CGSize @Binding var session: AVCaptureSession func makeUIView(context: Context) -> UIView { let view = UIViewType(frame: CGRect(origin: .zero, size: frameSize)) view.backgroundColor = .clear let cameraLayer = AVCaptureVideoPreviewLayer(session: session) cameraLayer.frame = .init(origin: .zero, size: frameSize) cameraLayer.videoGravity = .resizeAspectFill cameraLayer.masksToBounds = true view.layer.addSublayer(cameraLayer) return view } func updateUIView(_ uiView: UIViewType, context: Context) { } }
解决方案
方案1:修复CameraView的图层更新逻辑
Sheet弹出时会经历多次布局调整,原CameraView未跟随更新预览图层的frame,导致画面被隐藏。修改CameraView:
struct CameraView: UIViewRepresentable { var frameSize: CGSize @Binding var session: AVCaptureSession // 存储预览图层,方便后续更新 private var cameraLayer: AVCaptureVideoPreviewLayer? func makeUIView(context: Context) -> UIView { let view = UIView(frame: CGRect(origin: .zero, size: frameSize)) view.backgroundColor = .clear cameraLayer = AVCaptureVideoPreviewLayer(session: session) cameraLayer?.frame = view.bounds cameraLayer?.videoGravity = .resizeAspectFill cameraLayer?.masksToBounds = true if let layer = cameraLayer { view.layer.addSublayer(layer) } return view } func updateUIView(_ uiView: UIView, context: Context) { // 同步图层frame与Sheet最终布局 cameraLayer?.frame = uiView.bounds // 确保相机会话处于运行状态 if !session.isRunning { DispatchQueue.global(qos: .background).async { session.startRunning() } } } }
方案2:延迟启动相机会话
Sheet视图渲染需要时间,提前启动会话可能导致图层无法正确关联。修改ScannerView的setupCamera方法:
func setupCamera() { do { guard let device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera], mediaType: .video, position: .back).devices.first else { presentError("Unknown error.") return } let input = try AVCaptureDeviceInput(device: device) guard session.canAddInput(input), session.canAddOutput(barcodeOutput) else { presentError("Unknown error.") return } session.beginConfiguration() session.addInput(input) session.addOutput(barcodeOutput) barcodeOutput.metadataObjectTypes = [.upce, .ean8, .ean13] barcodeOutput.setMetadataObjectsDelegate(barcodeDelegate, queue: .main) session.commitConfiguration() // 延迟0.3秒启动会话,等待Sheet视图完全渲染 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.global(qos: .background).async { session.startRunning() } } } catch { presentError(error.localizedDescription) } }
方案3:设置Sheet为全屏(可选)
如果业务允许,让Sheet占据全屏,消除布局适配问题:
.sheet(isPresented: $showScanSheet) { ScannerView() .presentationDetents([.large]) .presentationDragIndicator(.hidden) }
原理说明
SwiftUI的.sheet是模态弹窗,其视图层级和生命周期与全屏视图存在差异:
- Sheet弹出时会多次调整布局,原代码中相机预览图层的frame未跟随更新,导致画面偏移或隐藏;
- 相机会话启动时机过早,此时Sheet的UIView尚未完成渲染,预览图层无法正确挂载到视图层级。
以上方案通过适配图层布局、延迟会话启动或调整Sheet样式,解决了相机预览不显示的问题。
内容的提问来源于stack exchange,提问作者Cedric
相关产品推荐
相关产品推荐

