复刻iOS相机App时的设备方向适配与AVCapture预览/录制方向问题咨询
嗨,我刚好做过原生相机的复刻需求,你的问题其实可以拆成UI布局方向适配和AVCapture预览/录制方向同步两个独立部分来解决,下面给你一步步梳理:
一、UI布局:竖屏锁定下仅特定子视图跟随设备旋转
原生iOS相机的核心布局逻辑是全局锁定竖屏界面方向,只有部分UI元素(比如快门按钮、切换摄像头按钮、闪光灯控件)会跟随设备方向旋转,整体布局不会因为设备横过来而重新计算。不管用UIKit还是SwiftUI,实现思路都是一致的:
UIKit实现思路
先锁定界面仅支持竖屏:
在你的相机ViewController里重写支持的方向属性:override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait }也可以在项目的Target配置里直接把支持的方向改成仅竖屏,效果是一样的。
监听设备方向变化:
开启设备方向通知,实时获取设备的旋转状态: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) }手动旋转目标子视图:
在通知回调里,根据当前设备方向计算旋转角度,给需要跟随旋转的控件设置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的逻辑来做:
- 先在项目配置里锁定仅支持竖屏,或者在
SceneDelegate/UIApplicationDelegate里设置支持的方向。 - 用
@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是竖屏锁定的,预览层的父容器是竖屏尺寸,就会出现黑边和画面拉伸的问题。
具体解决步骤
必须设置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 } }你可以在设备方向变化的回调里调用这个方法,确保录制方向实时同步。
调整预览层的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

