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

SwiftUI环境下如何裁剪AVCaptureVideoPreviewLayer?

解决方案:SwiftUI中实现可裁剪的摄像头预览视图

核心问题分析

你遇到的问题根源在于:

  1. 原UIViewControllerRepresentable封装的摄像头视图实际是全屏的,SwiftUI设置的frame只是视觉约束,并未真正缩小底层视图,导致预览画面偏移。
  2. 蒙版方式仅做视觉隐藏,底层预览层仍占满屏幕,会遮挡背景手势;同时坐标系不匹配,导致照片裁剪逻辑失效。

下面是适配SwiftUI的完整解决方案,让摄像头预览像普通SwiftUI视图一样使用,同时解决手势遮挡和照片裁剪问题。


1. 轻量封装摄像头预览视图(UIViewRepresentable)

用UIViewRepresentable替代UIViewControllerRepresentable,直接管理预览层的大小,确保和SwiftUI的frame完全匹配:

import SwiftUI
import AVFoundation

struct CameraPreview: UIViewRepresentable {
    @Binding var isSessionRunning: Bool
    let session: AVCaptureSession
    var previewLayer: AVCaptureVideoPreviewLayer? {
        return makeUIView(context: Context()).layer.sublayers?.first as? AVCaptureVideoPreviewLayer
    }
    
    func makeUIView(context: Context) -> UIView {
        let view = UIView(frame: .zero)
        let previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer.videoGravity = .resizeAspectFill
        previewLayer.frame = view.bounds
        previewLayer.masksToBounds = true
        view.layer.addSublayer(previewLayer)
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        // 实时更新预览层大小以匹配SwiftUI设置的frame
        if let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
            previewLayer.frame = uiView.bounds
        }
        // 控制会话生命周期
        if isSessionRunning && !session.isRunning {
            session.startRunning()
        } else if !isSessionRunning && session.isRunning {
            session.stopRunning()
        }
    }
}

2. 封装摄像头管理逻辑

创建CameraManager统一管理摄像头会话、拍照和裁剪逻辑,处理图像方向适配:

class CameraManager: ObservableObject {
    let session = AVCaptureSession()
    @Published var isSessionRunning = false
    @Published var capturedImage: UIImage?
    
    init() {
        setupCameraSession()
    }
    
    private func setupCameraSession() {
        session.sessionPreset = .photo
        
        guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
            print("无法获取后置摄像头")
            return
        }
        
        do {
            let input = try AVCaptureDeviceInput(device: backCamera)
            if session.canAddInput(input) {
                session.addInput(input)
            }
            
            // 添加照片输出
            let photoOutput = AVCapturePhotoOutput()
            if session.canAddOutput(photoOutput) {
                session.addOutput(photoOutput)
            }
        } catch {
            print("摄像头初始化错误: \(error.localizedDescription)")
        }
    }
    
    func capturePhoto(previewLayer: AVCaptureVideoPreviewLayer, previewFrame: CGRect) {
        guard let photoOutput = session.outputs.first as? AVCapturePhotoOutput else { return }
        
        let settings = AVCapturePhotoSettings()
        photoOutput.capturePhoto(with: settings, delegate: PhotoCaptureDelegate(manager: self, previewLayer: previewLayer, previewFrame: previewFrame))
    }
    
    // 照片捕获代理,处理裁剪逻辑
    private class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
        weak var manager: CameraManager?
        let previewLayer: AVCaptureVideoPreviewLayer
        let previewFrame: CGRect
        
        init(manager: CameraManager, previewLayer: AVCaptureVideoPreviewLayer, previewFrame: CGRect) {
            self.manager = manager
            self.previewLayer = previewLayer
            self.previewFrame = previewFrame
            super.init()
        }
        
        func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
            guard let data = photo.fileDataRepresentation(), let image = UIImage(data: data) else {
                print("照片捕获失败")
                return
            }
            manager?.capturedImage = cropImage(image, toPreviewLayer: previewLayer, withRect: previewFrame)
        }
        
        // 适配图像方向的裁剪方法
        private func cropImage(_ image: UIImage, toPreviewLayer layer: AVCaptureVideoPreviewLayer, withRect rect: CGRect) -> UIImage {
            let outputRect = layer.metadataOutputRectConverted(fromLayerRect: rect)
            guard let cgImage = image.cgImage else { return image }
            
            let width = CGFloat(cgImage.width)
            let height = CGFloat(cgImage.height)
            var cropRect = CGRect(
                x: outputRect.origin.x * width,
                y: outputRect.origin.y * height,
                width: outputRect.size.width * width,
                height: outputRect.size.height * height
            )
            
            // 修正图像方向导致的裁剪错位
            switch image.imageOrientation {
            case .right, .rightMirrored:
                cropRect = CGRect(
                    x: outputRect.origin.y * height,
                    y: (1 - outputRect.origin.x - outputRect.size.width) * width,
                    width: outputRect.size.height * height,
                    height: outputRect.size.width * width
                )
            case .left, .leftMirrored:
                cropRect = CGRect(
                    x: (1 - outputRect.origin.y - outputRect.size.height) * height,
                    y: outputRect.origin.x * width,
                    width: outputRect.size.height * height,
                    height: outputRect.size.width * width
                )
            case .down, .downMirrored:
                cropRect = CGRect(
                    x: (1 - outputRect.origin.x - outputRect.size.width) * width,
                    y: (1 - outputRect.origin.y - outputRect.size.height) * height,
                    width: outputRect.size.width * width,
                    height: outputRect.size.height * height
                )
            default: break
            }
            
            guard let croppedCGImage = cgImage.cropping(to: cropRect) else { return image }
            return UIImage(cgImage: croppedCGImage, scale: image.scale, orientation: image.imageOrientation)
        }
    }
}

3. SwiftUI中使用示例

像普通视图一样调用,同时支持手势穿透和正确裁剪:

struct ContentView: View {
    @StateObject private var cameraManager = CameraManager()
    @State private var previewFrame: CGRect = .zero
    
    var body: some View {
        ZStack {
            // 背景视图,测试手势是否正常响应
            Color.blue
                .onTapGesture { print("背景被点击") }
            
            // 摄像头预览,直接设置frame即可
            CameraPreview(isSessionRunning: $cameraManager.isSessionRunning, session: cameraManager.session)
                .frame(width: 200, height: 200)
                .cornerRadius(25)
                // 获取预览框的实际坐标,用于裁剪
                .overlay(
                    GeometryReader { proxy in
                        Color.clear
                            .onAppear { previewFrame = proxy.frame(in: .local) }
                            .onChange(of: proxy.frame(in: .local)) { previewFrame = $0 }
                    }
                )
            
            Button("拍照") {
                guard let previewLayer = cameraManager.session.previewLayer else { return }
                cameraManager.capturePhoto(previewLayer: previewLayer, previewFrame: previewFrame)
            }
            .foregroundColor(.white)
            .padding()
            .background(Color.black.opacity(0.7))
            .cornerRadius(10)
            .offset(y: 200)
        }
        .ignoresSafeArea()
        .onAppear { cameraManager.isSessionRunning = true }
        .onDisappear { cameraManager.isSessionRunning = false }
    }
}

关键改进点

  1. 真实裁剪而非视觉隐藏:预览层大小完全匹配SwiftUI设置的frame,不会遮挡背景手势。
  2. 坐标系正确转换:通过GeometryReader获取预览框的实际坐标,结合图像方向修正,确保裁剪区域和预览区域完全对应。
  3. 轻量封装:用UIViewRepresentable替代UIViewControllerRepresentable,减少不必要的层级,适配SwiftUI布局逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 19:20:55