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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 05:45:58