iOS16中iPhone旋转时AVFoundation PreviewLayer的正确设置
问题描述
使用AVFoundation开发录制应用时,竖屏预览正常,但iPhone旋转后预览画面出现错误旋转(如竖屏切右横屏时,画面中的'v'变成'<'),同时画面放大。尝试在UIViewController的viewDidLoad()、viewDidLayoutSubviews()、viewWillTransition()中调用updatePreviewLayerOrientation(),改用UIViewRepresentable及自定义DeviceRotationViewModifier均无效。
相关代码如下:
UIViewController实现
class VideoPreviewView: UIView { override class var layerClass: AnyClass { return AVCaptureVideoPreviewLayer.self } var videoPreviewLayer: AVCaptureVideoPreviewLayer { return layer as! AVCaptureVideoPreviewLayer } } class CameraViewController: UIViewController { private var previewView = VideoPreviewView() override func viewDidLoad() { super.viewDidLoad() previewView.videoPreviewLayer.session = CameraManager.shared.session previewView.videoPreviewLayer.videoGravity = .resizeAspectFill previewView.videoPreviewLayer.frame = view.bounds view.layer.addSublayer(previewView.videoPreviewLayer) updatePreviewLayerOrientation } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() } func updatePreviewLayerOrientation() { guard let connection = previewView.videoPreviewLayer.connection else { return } switch UIDevice.current.orientation { case .portrait: connection.videoOrientation = .portrait case .landscapeRight: connection.videoOrientation = .landscapeLeft case .landscapeLeft: connection.videoOrientation = .landscapeRight case .portraitUpsideDown: connection.videoOrientation = .portraitUpsideDown default: connection.videoOrientation = .portrait } previewView.videoPreviewLayer.frame = view.bounds } }
UIViewRepresentable实现
struct CameraViewRepresentable: UIViewRepresentable { func makeUIView(context: Context) -> VideoPreviewView { let view = VideoPreviewView() view.backgroundColor = .black view.videoPreviewLayer.session = CameraManager.shared.session view.videoPreviewLayer.videoGravity = .resizeAspectFill CameraManager.shared.videoPreview = view return view } public func updateUIView(_ uiView: VideoPreviewView, context: Context) { } class VideoPreviewView: UIView { override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } var videoPreviewLayer: AVCaptureVideoPreviewLayer { return layer as! AVCaptureVideoPreviewLayer } } }
自定义旋转监听Modifier
struct DeviceRotationViewModifier: ViewModifier { let action: (UIDeviceOrientation) -> Void func body(content: Content) -> some View { content .onAppear() .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in action(UIDevice.current.orientation) } } } // A View wrapper to make the modifier easier to use extension View { func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { self.modifier(DeviceRotationViewModifier(action: action)) } }
正确的预览层方向设置方案
1. 修正UIViewController中预览视图的添加方式
不要直接将AVCaptureVideoPreviewLayer添加到控制器view的layer上,而是将VideoPreviewView作为子视图添加,让布局系统正确管理其尺寸:
override func viewDidLoad() { super.viewDidLoad() previewView.videoPreviewLayer.session = CameraManager.shared.session previewView.videoPreviewLayer.videoGravity = .resizeAspectFill view.addSubview(previewView) // 设置约束让previewView铺满整个view previewView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ previewView.topAnchor.constraint(equalTo: view.topAnchor), previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor), previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor), previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) updatePreviewLayerOrientation() }
2. 优化方向更新逻辑,使用界面方向而非设备方向
UIDevice.current.orientation可能包含未知、FaceUp/FaceDown等无效状态,改用控制器的interfaceOrientation更可靠,同时要检查连接是否支持视频方向:
func updatePreviewLayerOrientation() { guard let connection = previewView.videoPreviewLayer.connection, connection.isVideoOrientationSupported else { return } let orientation = interfaceOrientation switch orientation { case .portrait: connection.videoOrientation = .portrait case .landscapeRight: connection.videoOrientation = .landscapeRight case .landscapeLeft: connection.videoOrientation = .landscapeLeft case .portraitUpsideDown: connection.videoOrientation = .portraitUpsideDown default: connection.videoOrientation = .portrait } }
3. 在合适的时机触发方向更新
- 在
viewDidLayoutSubviews中更新预览层尺寸 - 在
viewWillTransition中配合过渡动画更新方向:
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() previewView.videoPreviewLayer.frame = previewView.bounds } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate { _ in self.updatePreviewLayerOrientation() } completion: { _ in self.updatePreviewLayerOrientation() } }
4. SwiftUI中UIViewRepresentable的正确实现
让VideoPreviewView自身监听旋转通知,同时确保预览层尺寸正确:
struct CameraViewRepresentable: UIViewRepresentable { func makeUIView(context: Context) -> VideoPreviewView { let view = VideoPreviewView() view.backgroundColor = .black view.videoPreviewLayer.session = CameraManager.shared.session view.videoPreviewLayer.videoGravity = .resizeAspectFill // 监听旋转通知 NotificationCenter.default.addObserver( view, selector: #selector(view.updateOrientation), name: UIDevice.orientationDidChangeNotification, object: nil ) // 初始设置方向 view.updateOrientation() return view } func updateUIView(_ uiView: VideoPreviewView, context: Context) { // 每次更新时确保尺寸正确 uiView.videoPreviewLayer.frame = uiView.bounds } class VideoPreviewView: UIView { override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } var videoPreviewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer } @objc func updateOrientation() { guard let connection = videoPreviewLayer.connection, connection.isVideoOrientationSupported else { return } let deviceOrientation = UIDevice.current.orientation switch deviceOrientation { case .portrait: connection.videoOrientation = .portrait case .landscapeRight: connection.videoOrientation = .landscapeLeft case .landscapeLeft: connection.videoOrientation = .landscapeRight case .portraitUpsideDown: connection.videoOrientation = .portraitUpsideDown default: connection.videoOrientation = .portrait } videoPreviewLayer.frame = bounds } deinit { NotificationCenter.default.removeObserver(self) } } }
关键注意点
- 确保
AVCaptureVideoPreviewLayer的frame始终与父视图(VideoPreviewView)的bounds一致,避免拉伸放大 - 优先使用
interfaceOrientation(UIKit)或场景方向(SwiftUI)判断界面方向,而非设备方向 - 必须检查
connection.isVideoOrientationSupported,避免在不支持的设备/会话上崩溃
内容的提问来源于stack exchange,提问作者grass
相关产品推荐
相关产品推荐

