View like the camera app

Hello all,

I would like to understand how to create a SwiftUI View like the official camera app. When the device orientation changes, the view is not animating, and the buttons just rotate (4:3, 1x...).

The camera app view is compose by flash and live buttons, camera preview, config buttons, and big button to shot the photo. In portrait, it is from top to bottom, in landscape, from left to right.

Also, when the last pictures view is shown, it is adapted to the current orientation, like if the camera preview view was rendered in the same device orientation.

Ideas?

Thanks!

Accepted Reply

Hello,

I implemented a solution that fits my requirements. If you are interested on it, here is the code: https://github.com/heltena/Orientation

Basically, I'm showing a sheet using UIKit, and controlling the supportedInterfaceOrientations in the UIViewController that is shown.

For lazy people, here is the code:


//
//  RestrictedInterfaceOrientationSheet.swift
//  Orientation
//
//  Created by Heliodoro Tejedor Navarro on 1/3/23.
//

import SwiftUI

public enum ModalTransitionStyle: Int, @unchecked Sendable {
    case coverVertical = 0
    case flipHorizontal = 1
    case crossDissolve = 2
    
    fileprivate var uiKitVersion: UIModalTransitionStyle {
        return UIModalTransitionStyle(rawValue: self.rawValue)!
    }
}

struct RestrictedInterfaceOrientationSheet<SheetContent: View>: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    var restrictInterfaceOrientationTo: UIInterfaceOrientationMask
    var modalTransitionStyle: ModalTransitionStyle
    var animated: Bool
    var content: () -> SheetContent
    
    func makeUIViewController(context: Context) -> InternalViewController {
        InternalViewController()
    }
    
    func updateUIViewController(_ uiViewController: InternalViewController, context: Context) {
        if !isPresented {
            uiViewController.presentedViewController?.dismiss(animated: animated)
            return
        }
        
        if uiViewController.presentedViewController == nil {
            let sheetViewController = PortraitHostingViewController(restrictInterfaceOrientationTo: restrictInterfaceOrientationTo, rootView: content())
            sheetViewController.modalPresentationStyle = .overFullScreen // don't use .fullscreen, so it will remove the parent from the view hierarchy!
            sheetViewController.modalTransitionStyle = modalTransitionStyle.uiKitVersion
            uiViewController.present(sheetViewController, animated: animated)
            return
        }
        
        if let presentedViewController = uiViewController.presentedViewController as? UIHostingController<SheetContent> {
            presentedViewController.rootView = content()
            return
        }

        fatalError("Invalid parent when dismissing a presented view controller")
    }

    class InternalViewController: UIViewController {
        override func loadView() {
            super.loadView()
            view.backgroundColor = .clear
        }
    }
    
    class PortraitHostingViewController<Content: View>: UIHostingController<Content> {
        var restrictInterfaceOrientationTo: UIInterfaceOrientationMask
        
        init(restrictInterfaceOrientationTo: UIInterfaceOrientationMask, rootView: Content) {
            self.restrictInterfaceOrientationTo = restrictInterfaceOrientationTo
            super.init(rootView: rootView)
        }
        
        @MainActor required dynamic init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            restrictInterfaceOrientationTo
        }
    }
}

struct RestrictedInterfaceOrientationSheetViewModifier<SheetContent: View>: ViewModifier {
    @Binding var isPresented: Bool
    var restrictInterfaceOrientationTo: UIInterfaceOrientationMask
    var modalTransitionStyle: ModalTransitionStyle
    var animated: Bool
    @ViewBuilder var content: () -> SheetContent
    
    func body(content: Content) -> some View {
        content
            .background {
                RestrictedInterfaceOrientationSheet(
                    isPresented: $isPresented,
                    restrictInterfaceOrientationTo: restrictInterfaceOrientationTo,
                    modalTransitionStyle: modalTransitionStyle,
                    animated: animated,
                    content: self.content)
            }
    }
}

extension View {
    public func sheet<SheetContent: View>(isPresented: Binding<Bool>, restrictInterfaceOrientationTo: UIInterfaceOrientationMask = .portrait, modalTransitionStyle: ModalTransitionStyle = .crossDissolve, animated: Bool = true, onDismiss: (() -> Void)?, @ViewBuilder content: @escaping () -> SheetContent) -> some View {
        self.modifier(
            RestrictedInterfaceOrientationSheetViewModifier(
                isPresented: isPresented,
                restrictInterfaceOrientationTo: restrictInterfaceOrientationTo,
                modalTransitionStyle: modalTransitionStyle,
                animated: animated,
                content: content))
    }
}

And how to use it:

struct ContentView: View {
    @Binding var document: OrientationDocument
    @State var showCamera = false
    
    var body: some View {
        Form {
            Section {
                Text(document.text)
            } header: {
                Text("Example")
            }
        }
        .toolbar {
            Button {
                showCamera = true
            } label: {
                Label("Camera", systemImage: "camera")
            }
        }
        .orientationLockModifier(mask: .portrait)
        .sheet(isPresented: $showCamera, restrictInterfaceOrientationTo: .portrait, modalTransitionStyle: .crossDissolve, animated: false) {
            print("bye bye")
        } content: {
            VStack(spacing: 20) {
                Spacer()
                Text("Show Camera Preview here")
                Button {
                    showCamera = false
                } label: {
                    Text("Close")
                }
                Spacer()
            }
        }
    }
}

Replies

Hello,

I implemented a solution that fits my requirements. If you are interested on it, here is the code: https://github.com/heltena/Orientation

Basically, I'm showing a sheet using UIKit, and controlling the supportedInterfaceOrientations in the UIViewController that is shown.

For lazy people, here is the code:


//
//  RestrictedInterfaceOrientationSheet.swift
//  Orientation
//
//  Created by Heliodoro Tejedor Navarro on 1/3/23.
//

import SwiftUI

public enum ModalTransitionStyle: Int, @unchecked Sendable {
    case coverVertical = 0
    case flipHorizontal = 1
    case crossDissolve = 2
    
    fileprivate var uiKitVersion: UIModalTransitionStyle {
        return UIModalTransitionStyle(rawValue: self.rawValue)!
    }
}

struct RestrictedInterfaceOrientationSheet<SheetContent: View>: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    var restrictInterfaceOrientationTo: UIInterfaceOrientationMask
    var modalTransitionStyle: ModalTransitionStyle
    var animated: Bool
    var content: () -> SheetContent
    
    func makeUIViewController(context: Context) -> InternalViewController {
        InternalViewController()
    }
    
    func updateUIViewController(_ uiViewController: InternalViewController, context: Context) {
        if !isPresented {
            uiViewController.presentedViewController?.dismiss(animated: animated)
            return
        }
        
        if uiViewController.presentedViewController == nil {
            let sheetViewController = PortraitHostingViewController(restrictInterfaceOrientationTo: restrictInterfaceOrientationTo, rootView: content())
            sheetViewController.modalPresentationStyle = .overFullScreen // don't use .fullscreen, so it will remove the parent from the view hierarchy!
            sheetViewController.modalTransitionStyle = modalTransitionStyle.uiKitVersion
            uiViewController.present(sheetViewController, animated: animated)
            return
        }
        
        if let presentedViewController = uiViewController.presentedViewController as? UIHostingController<SheetContent> {
            presentedViewController.rootView = content()
            return
        }

        fatalError("Invalid parent when dismissing a presented view controller")
    }

    class InternalViewController: UIViewController {
        override func loadView() {
            super.loadView()
            view.backgroundColor = .clear
        }
    }
    
    class PortraitHostingViewController<Content: View>: UIHostingController<Content> {
        var restrictInterfaceOrientationTo: UIInterfaceOrientationMask
        
        init(restrictInterfaceOrientationTo: UIInterfaceOrientationMask, rootView: Content) {
            self.restrictInterfaceOrientationTo = restrictInterfaceOrientationTo
            super.init(rootView: rootView)
        }
        
        @MainActor required dynamic init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            restrictInterfaceOrientationTo
        }
    }
}

struct RestrictedInterfaceOrientationSheetViewModifier<SheetContent: View>: ViewModifier {
    @Binding var isPresented: Bool
    var restrictInterfaceOrientationTo: UIInterfaceOrientationMask
    var modalTransitionStyle: ModalTransitionStyle
    var animated: Bool
    @ViewBuilder var content: () -> SheetContent
    
    func body(content: Content) -> some View {
        content
            .background {
                RestrictedInterfaceOrientationSheet(
                    isPresented: $isPresented,
                    restrictInterfaceOrientationTo: restrictInterfaceOrientationTo,
                    modalTransitionStyle: modalTransitionStyle,
                    animated: animated,
                    content: self.content)
            }
    }
}

extension View {
    public func sheet<SheetContent: View>(isPresented: Binding<Bool>, restrictInterfaceOrientationTo: UIInterfaceOrientationMask = .portrait, modalTransitionStyle: ModalTransitionStyle = .crossDissolve, animated: Bool = true, onDismiss: (() -> Void)?, @ViewBuilder content: @escaping () -> SheetContent) -> some View {
        self.modifier(
            RestrictedInterfaceOrientationSheetViewModifier(
                isPresented: isPresented,
                restrictInterfaceOrientationTo: restrictInterfaceOrientationTo,
                modalTransitionStyle: modalTransitionStyle,
                animated: animated,
                content: content))
    }
}

And how to use it:

struct ContentView: View {
    @Binding var document: OrientationDocument
    @State var showCamera = false
    
    var body: some View {
        Form {
            Section {
                Text(document.text)
            } header: {
                Text("Example")
            }
        }
        .toolbar {
            Button {
                showCamera = true
            } label: {
                Label("Camera", systemImage: "camera")
            }
        }
        .orientationLockModifier(mask: .portrait)
        .sheet(isPresented: $showCamera, restrictInterfaceOrientationTo: .portrait, modalTransitionStyle: .crossDissolve, animated: false) {
            print("bye bye")
        } content: {
            VStack(spacing: 20) {
                Spacer()
                Text("Show Camera Preview here")
                Button {
                    showCamera = false
                } label: {
                    Text("Close")
                }
                Spacer()
            }
        }
    }
}

Hello, I've been trying to find a solution for this as well and currently only found UIKit implementations that sadly didn't work when applied to SwiftUI. But I managed to gather a solution that seems a bit more SwiftUI friendly, you can check it out here. Hope this helps : )