SwiftUI中相机预览视图添加CALayer不显示的问题排查
问题
开发基于SwiftUI的目标检测应用时,将自定义CALayer添加到相机预览视图后无法在屏幕上显示。
相机会话配置代码
func configureSession() { if setupResult != .success { return }session.beginConfiguration() session.sessionPreset = .vga640x480 // Add video input. do { var defaultVideoDevice: AVCaptureDevice? if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { // If a rear dual camera is not available, default to the rear wide angle camera. defaultVideoDevice = backCameraDevice } else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) { // If the rear wide angle camera isn't available, default to the front wide angle camera. defaultVideoDevice = frontCameraDevice } guard let videoDevice = defaultVideoDevice else { print("Default video device is unavailable.") setupResult = .configurationFailed session.commitConfiguration() return } let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) if session.canAddInput(videoDeviceInput) { session.addInput(videoDeviceInput) self.videoDeviceInput = videoDeviceInput } else { print("Couldn't add video device input to the session.") setupResult = .configurationFailed session.commitConfiguration() return } } catch { print("Couldn't create video device input: \(error)") setupResult = .configurationFailed session.commitConfiguration() return } if session.canAddOutput(videoDataOutput) { session.addOutput(videoDataOutput) // Add a video data output videoDataOutput.alwaysDiscardsLateVideoFrames = true videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)] videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue) } else { print("Could not add video data output to the session") session.commitConfiguration() return } let captureConnection = videoDataOutput.connection(with: .video) // Always process the frames captureConnection?.isEnabled = true do { try videoDevice!.lockForConfiguration() let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!) bufferSize.width = CGFloat(dimensions.width) bufferSize.height = CGFloat(dimensions.height) videoDevice!.unlockForConfiguration() } catch { print(error) } session.commitConfiguration() self.isConfigured = true DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.session.startRunning() }
尝试添加CALayer的代码
func configure() { configureSession() print("sublayers before add \(self.view.layer.sublayers!.count)") // =1 let circleLayer = CALayer() circleLayer.name="Younus" circleLayer.frame = CGRect(x: 200, y: 200, width: 400, height: 400) circleLayer.backgroundColor = UIColor.green.cgColor self.view.layer.addSublayer(circleLayer) print("sublayers before add \(self.view.layer.sublayers!.count)")==2 for sublayer in view.layer.sublayers! { print(sublayer.name) } } }
相机预览视图实现代码
struct CameraPreview: UIViewRepresentable { class VideoPreviewView: UIView { override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } var videoPreviewLayer: AVCaptureVideoPreviewLayer { return layer as! AVCaptureVideoPreviewLayer } } let session: AVCaptureSession func makeUIView(context: Context) -> VideoPreviewView { let view = VideoPreviewView() view.backgroundColor = .black view.videoPreviewLayer.cornerRadius = 0 view.videoPreviewLayer.session = session view.videoPreviewLayer.connection?.videoOrientation = .portrait return view } func updateUIView(_ uiView: VideoPreviewView, context: Context) { } } struct CameraPreview_Previews: PreviewProvider { static var previews: some View { CameraPreview(session: AVCaptureSession()) .frame(height: 20) } }
问题原因
- 层级覆盖:你将
circleLayer添加到了视图控制器的根view.layer上,但CameraPreview作为独立的UIViewRepresentable视图,其内部的AVCaptureVideoPreviewLayer会覆盖在根视图的子层之上,导致自定义layer被相机预览层遮挡。 - 坐标系不匹配:相机预览的分辨率(如
.vga640x480)与屏幕坐标系存在缩放、旋转差异,直接设置固定的CGRect可能让layer超出预览视图的可见区域。
解决方法
方法一:将自定义Layer添加到相机预览层的子层
修改CameraPreview,把自定义layer直接添加到AVCaptureVideoPreviewLayer的子层,确保其显示在相机画面之上:
struct CameraPreview: UIViewRepresentable { class VideoPreviewView: UIView { override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } var videoPreviewLayer: AVCaptureVideoPreviewLayer { return layer as! AVCaptureVideoPreviewLayer } } let session: AVCaptureSession let overlayLayer: CALayer? // 传入自定义layer func makeUIView(context: Context) -> VideoPreviewView { let view = VideoPreviewView() view.backgroundColor = .black view.videoPreviewLayer.cornerRadius = 0 view.videoPreviewLayer.session = session view.videoPreviewLayer.connection?.videoOrientation = .portrait // 将自定义layer添加到预览层 if let overlayLayer = overlayLayer { view.videoPreviewLayer.addSublayer(overlayLayer) } return view } func updateUIView(_ uiView: VideoPreviewView, context: Context) { // 动态更新layer的话可在此处处理 } }
在配置方法中创建layer并传入CameraPreview:
func configure() { configureSession() let circleLayer = CALayer() circleLayer.name = "Younus" // 基于预览视图尺寸设置frame,避免超出可见区域 circleLayer.frame = CGRect(x: 100, y: 100, width: 200, height: 200) circleLayer.backgroundColor = UIColor.green.cgColor // 在SwiftUI视图中使用时传入layer // CameraPreview(session: session, overlayLayer: circleLayer) }
方法二:用SwiftUI原生视图替代CALayer
既然是SwiftUI项目,推荐直接用SwiftUI视图叠加在相机预览之上,避免UIKit与SwiftUI的层级冲突:
struct CameraView: View { let session: AVCaptureSession var body: some View { ZStack { CameraPreview(session: session) .ignoresSafeArea() // 用SwiftUI视图替代CALayer Circle() .fill(Color.green.opacity(0.5)) .frame(width: 200, height: 200) .position(x: UIScreen.main.bounds.width/2, y: UIScreen.main.bounds.height/2) } } }
额外注意事项
- 若需基于目标检测结果动态更新位置,方法一中可通过
UIViewRepresentable的Coordinator监听结果,在updateUIView中调整layer的frame;方法二中直接绑定数据更新SwiftUI视图位置即可。 - 相机预览层的
videoGravity属性会影响画面缩放,设置为.resizeAspect或.resizeAspectFill时,需确保自定义layer的frame与预览画面的实际显示区域匹配。
内容的提问来源于stack exchange,提问作者Younis
相关产品推荐
相关产品推荐

