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

复刻iOS相机App时的设备方向适配与AVCapture预览/录制方向问题咨询

复刻iOS相机App时的设备方向适配与AVCapture预览/录制方向问题咨询

嗨,我刚好做过原生相机的复刻需求,你的问题其实可以拆成UI布局方向适配和AVCapture预览/录制方向同步两个独立部分来解决,下面给你一步步梳理:

一、UI布局:竖屏锁定下仅特定子视图跟随设备旋转

原生iOS相机的核心布局逻辑是全局锁定竖屏界面方向,只有部分UI元素(比如快门按钮、切换摄像头按钮、闪光灯控件)会跟随设备方向旋转,整体布局不会因为设备横过来而重新计算。不管用UIKit还是SwiftUI,实现思路都是一致的:

UIKit实现思路

  1. 先锁定界面仅支持竖屏:
    在你的相机ViewController里重写支持的方向属性:

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
    }
    

    也可以在项目的Target配置里直接把支持的方向改成仅竖屏,效果是一样的。

  2. 监听设备方向变化:
    开启设备方向通知,实时获取设备的旋转状态:

    override func viewDidLoad() {
        super.viewDidLoad()
        // 开启设备方向监听
        UIDevice.current.beginGeneratingDeviceOrientationNotifications()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(onDeviceOrientationChanged),
            name: UIDevice.orientationDidChangeNotification,
            object: nil
        )
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // 记得移除监听,避免内存泄漏
        UIDevice.current.endGeneratingDeviceOrientationNotifications()
        NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
    }
    
  3. 手动旋转目标子视图:
    在通知回调里,根据当前设备方向计算旋转角度,给需要跟随旋转的控件设置transform属性:

    @objc private func onDeviceOrientationChanged() {
        let orientation = UIDevice.current.orientation
        // 排除无效的设备方向(比如设备平放)
        guard orientation.isValidInterfaceOrientation else { return }
        
        var rotationAngle: CGFloat = 0
        switch orientation {
        case .landscapeLeft:
            rotationAngle = .pi / 2
        case .landscapeRight:
            rotationAngle = -.pi / 2
        case .portraitUpsideDown:
            rotationAngle = .pi
        default:
            rotationAngle = 0
        }
        
        // 给需要旋转的子视图设置transform,比如快门按钮、闪光灯按钮
        shutterButton.transform = CGAffineTransform(rotationAngle: rotationAngle)
        flashButton.transform = CGAffineTransform(rotationAngle: rotationAngle)
    }
    

SwiftUI实现思路

SwiftUI里没有直接的界面方向锁定API,需要结合UIKit的逻辑来做:

  1. 先在项目配置里锁定仅支持竖屏,或者在SceneDelegate/UIApplicationDelegate里设置支持的方向。
  2. 用@State变量记录旋转角度,通过监听设备方向通知更新这个变量:
    import SwiftUI
    
    struct CameraView: View {
        @State private var rotationAngle: Double = 0
        
        var body: some View {
            ZStack {
                // 固定竖屏的主布局容器(比如预览层的父视图、底部工具栏框架)
                Color.black.edgesIgnoringSafeArea(.all)
                
                // 需要跟随旋转的子视图,比如快门按钮
                Button(action: { /* 拍照逻辑 */ }) {
                    Circle()
                        .frame(width: 60, height: 60)
                        .foregroundColor(.white)
                        .overlay(Circle().stroke(Color.black, lineWidth: 2))
                }
                .rotationEffect(.degrees(rotationAngle))
                .position(x: UIScreen.main.bounds.width/2, y: UIScreen.main.bounds.height - 80)
                
                // 其他需要旋转的控件同理,比如闪光灯按钮
                Button(action: { /* 切换闪光灯逻辑 */ }) {
                    Image(systemName: "bolt.fill")
                        .foregroundColor(.white)
                        .font(.title)
                }
                .rotationEffect(.degrees(rotationAngle))
                .position(x: 40, y: UIScreen.main.bounds.height - 80)
            }
            .onAppear {
                UIDevice.current.beginGeneratingDeviceOrientationNotifications()
                NotificationCenter.default.addObserver(
                    forName: UIDevice.orientationDidChangeNotification,
                    object: nil,
                    queue: .main
                ) { _ in
                    let orientation = UIDevice.current.orientation
                    guard orientation.isValidInterfaceOrientation else { return }
                    
                    switch orientation {
                    case .landscapeLeft:
                        rotationAngle = 90
                    case .landscapeRight:
                        rotationAngle = -90
                    case .portraitUpsideDown:
                        rotationAngle = 180
                    default:
                        rotationAngle = 0
                    }
                }
            }
            .onDisappear {
                UIDevice.current.endGeneratingDeviceOrientationNotifications()
                NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
            }
        }
    }
    
    核心就是主布局完全不响应设备方向,只有标记的子视图通过rotationEffect跟随设备角度旋转,和原生相机的表现完全一致。

二、AVCapture预览与录制方向的解耦

这是你遇到的核心痛点:设置AVCaptureVideoOrientation后,预览会出现黑边、画面旋转错误;不设置的话,录制/推流的视频方向又不对。其实解决方案的核心是让预览层的变换抵消掉videoOrientation带来的旋转,同时保持录制的视频方向正确。

问题根源

当你给AVCaptureConnection设置videoOrientation时,AVCaptureSession会自动调整预览层的输出比例来匹配这个方向,但因为你的App是竖屏锁定的,预览层的父容器是竖屏尺寸,就会出现黑边和画面拉伸的问题。

具体解决步骤

  1. 必须设置AVCaptureConnection的videoOrientation(保证录制方向正确)
    不管预览怎么处理,你都需要根据设备方向实时更新AVCaptureConnection的videoOrientation,这样录制的视频元数据里的方向信息才是正确的,播放时会自动适配:

    func updateCaptureOrientation() {
        guard let connection = previewLayer.connection, connection.isVideoOrientationSupported else { return }
        let deviceOrientation = UIDevice.current.orientation
        // 把设备方向转成AVCaptureVideoOrientation
        guard let videoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue) else { return }
        // 过滤无效方向
        if [.portrait, .portraitUpsideDown, .landscapeLeft, .landscapeRight].contains(videoOrientation) {
            connection.videoOrientation = videoOrientation
        }
    }
    

    你可以在设备方向变化的回调里调用这个方法,确保录制方向实时同步。

  2. 调整预览层的transform,抵消videoOrientation的旋转
    当你设置了connection.videoOrientation后,预览层的画面会跟着旋转,这时候你需要给previewLayer的transform设置一个反向的旋转,让画面看起来是正的,同时保持录制的视频方向正确:

    func adjustPreviewLayerTransform(for orientation: AVCaptureVideoOrientation) {
        var transform = CATransform3DIdentity
        // 根据videoOrientation设置反向旋转
        switch orientation {
        case .landscapeLeft:
            transform = CATransform3DRotate(transform, -.pi / 2, 0, 0, 1)
        case .landscapeRight:
            transform = CATransform3DRotate(transform, .pi / 2, 0, 0, 1)
        case .portraitUpsideDown:
            transform = CATransform3DRotate(transform, .pi, 0, 0, 1)
        default:
            transform = CATransform3DIdentity
        }
        // 应用变换,同时让预览层充满父容器
        previewLayer.transform = transform
        previewLayer.frame = previewContainerView.bounds
    }
    

    然后在你更新connection.videoOrientation之后,立刻调用这个方法:

    @objc private func onDeviceOrientationChanged() {
        // ... 之前的UI旋转逻辑 ...
        updateCaptureOrientation()
        if let orientation = AVCaptureVideoOrientation(rawValue: UIDevice.current.orientation.rawValue) {
            adjustPreviewLayerTransform(for: orientation)
        }
    }
    

这样处理后,录制的视频方向是正确的,预览层的画面也会始终保持正立且充满容器,完全复刻原生相机的表现。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 11:18:10