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

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是模态弹窗,其视图层级和生命周期与全屏视图存在差异:

  1. Sheet弹出时会多次调整布局,原代码中相机预览图层的frame未跟随更新,导致画面偏移或隐藏;
  2. 相机会话启动时机过早,此时Sheet的UIView尚未完成渲染,预览图层无法正确挂载到视图层级。

以上方案通过适配图层布局、延迟会话启动或调整Sheet样式,解决了相机预览不显示的问题。

内容的提问来源于stack exchange,提问作者Cedric

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 05:05:57