Search code examples
iosswiftuiavfoundationscreen-orientation

How to handle a device rotation for AVCaptureVideoPreviewLayer?


I have a simple camera preview implementation:

import SwiftUI
import AVFoundation

struct CameraView: View {
    @StateObject var model = CameraModel()
    var body: some View {
        CameraPreview(camera: model)
            .safeAreaInset(edge: .bottom, alignment: .center, spacing: 0) {
                Color.clear
                    .frame(height: 0)
                    .background(Material.bar)
            }
            .ignoresSafeArea(.all, edges: .top)
            .onAppear() {
                model.check()
            }
    }
}

struct CameraPreview: UIViewRepresentable {
    @ObservedObject var camera: CameraModel
    
    func makeUIView(context: Context) -> some UIView {
        let view = UIView(frame: UIScreen.main.bounds)
        camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
        camera.preview.videoGravity = AVLayerVideoGravity.resizeAspectFill
        camera.preview.frame = view.frame
        view.layer.addSublayer(camera.preview)
        camera.start()
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

struct CameraView_Previews: PreviewProvider {
    static var previews: some View {
        CameraView()
    }
}

class CameraModel: ObservableObject {
    @Published var session = AVCaptureSession()
    @Published var alert = false
    @Published var preview: AVCaptureVideoPreviewLayer!
    
    func check() {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            setUp()
            break
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { (status) in
                if status {
                    self.setUp()
                }
            }
            break
        case .denied:
            self.alert.toggle()
            break
        default:
            break
        }
    }
    
    func setUp() {
        do {
            self.session.beginConfiguration()
            let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back)
            let input = try AVCaptureDeviceInput(device: device!)
            
            if self.session.canAddInput(input) {
                self.session.addInput(input)
            }
            
            self.session.commitConfiguration()
        }
        catch {
            print(error.localizedDescription)
        }
    }
    
    func start() {
        self.session.startRunning()
    }
}

The problem is that it doesn't handle screen rotations:

Screenshot

I found similar topics, for example, this one, but I am a noobie in iOS development, I can't even understand where to put this solution. I've checked neither View, nor UIViewRepresentable have such methods to override.

How to handle screen rotation in AVCaptureVideoPreviewLayer?


Solution

  • This is a working variant with video rotation based on Dscyre Scotti'es answer:

    struct CameraView: View {
        @StateObject var model = CameraModel()
        var body: some View {
            CameraPreview(camera: model)
                .safeAreaInset(edge: .bottom, alignment: .center, spacing: 0) {
                    Color.clear
                        .frame(height: 0)
                        .background(Material.bar)
                }
                .ignoresSafeArea(.all, edges: [.top, .horizontal])
                .onAppear() {
                    model.check()
                }
        }
    }
    
    struct CameraPreview: UIViewRepresentable {
        @ObservedObject var camera: CameraModel
        
        class LayerView: UIView {
            var parent: CameraPreview! = nil
            
            override func layoutSubviews() {
                super.layoutSubviews()
                // To disable default animation of layer. You can comment out those lines with `CATransaction` if you want to include
                CATransaction.begin()
                CATransaction.setDisableActions(true)
                layer.sublayers?.forEach({ layer in
                    layer.frame = UIScreen.main.bounds
                })
                self.parent.camera.rotate(orientation: UIDevice.current.orientation)
                CATransaction.commit()
            }
        }
        
        func makeUIView(context: Context) -> some UIView {
            let view = LayerView()
            view.parent = self
            camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
            camera.preview.videoGravity = AVLayerVideoGravity.resizeAspectFill
            camera.preview.frame = view.frame
            view.layer.addSublayer(camera.preview)
            camera.start()
            return view
        }
        
        func updateUIView(_ uiView: UIViewType, context: Context) {
        }
    }
    
    struct CameraView_Previews: PreviewProvider {
        static var previews: some View {
            CameraView()
        }
    }
    
    class CameraModel: ObservableObject {
        @Published var session = AVCaptureSession()
        @Published var alert = false
        @Published var preview: AVCaptureVideoPreviewLayer!
        
        func check() {
            switch AVCaptureDevice.authorizationStatus(for: .video) {
            case .authorized:
                setUp()
                break
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: .video) { (status) in
                    if status {
                        self.setUp()
                    }
                }
                break
            case .denied:
                self.alert.toggle()
                break
            default:
                break
            }
        }
        
        func setUp() {
            do {
                self.session.beginConfiguration()
                let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back)
                let input = try AVCaptureDeviceInput(device: device!)
                
                if self.session.canAddInput(input) {
                    self.session.addInput(input)
                }
                
                self.session.commitConfiguration()
            }
            catch {
                print(error.localizedDescription)
            }
        }
        
        func start() {
            self.session.startRunning()
        }
        
        func rotate(orientation: UIDeviceOrientation) {
            let videoConnection = self.preview.connection
            switch orientation {
            case .portraitUpsideDown:
                videoConnection?.videoOrientation = .portraitUpsideDown
            case .landscapeLeft:
                videoConnection?.videoOrientation = .landscapeRight
            case .landscapeRight:
                videoConnection?.videoOrientation = .landscapeLeft
            case .faceDown:
                videoConnection?.videoOrientation = .portraitUpsideDown
            default:
                videoConnection?.videoOrientation = .portrait
            }
        }
    }