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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 21:14:57