skip to Main Content

I am trying to present a view in landscape orientation from a portrait view using the below code

   Button {
      isLandscapeViewPresented.toggle()
   } label: {
      Text("Click for landscape view")
   }.fullScreenCover(isPresented: $isLandscapeViewPresented) {
     LandscapeOnlyView {
        LandscapeView()
     }
   }

And in LandscapeOnlyView wrapper I have as below

struct LandscapeOnlyView<Content: View>: UIViewControllerRepresentable {
    
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
    
    
    func makeUIViewController(context: Context) -> some UIViewController {
        return LandscapeHostingController(rootView: content)
    }
}

For LandscapeHostingController which holds the rootview as below:

class LandscapeHostingController<Content: View>: UIHostingController<Content> {
        
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .landscape
    }
        
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        AppDelegate.orientationLock = .landscape
        let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        scene?.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight), errorHandler: { error in
            print(error)
        })
    }
        
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        AppDelegate.orientationLock = .portrait
        let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        scene?.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait), errorHandler: { error in
            print(error)
        })
    }
}

I am using AppDelegate, as you can see where I update the orientationLock, with supportedInterfaceOrientations override returning the updated orientation whenever I need to change orientations.

I am getting the below output.

Sample

I need the LandscapeView to be in landscape even before presenting unlike what is show above where the view switches to landscape as the orientation update is done in viewWillAppear of the LandscapeHostingController wrapper for a swift ui view.

I have tried many ways. Is there something wrong in my approach which should be changed.

Any ideas on how that it be achieved?

2

Answers


  1. To lock the orientation of a view to only landscape in a SwiftUI app, you need to manage the orientation in a UIViewController or via scene-based updates. Below is a simplified approach that ensures the view is locked in landscape before it appears.

    Code Example:

    1. Create a custom UIViewController to manage landscape orientation:
    import SwiftUI
    
    struct LandscapeOnlyView<Content: View>: UIViewControllerRepresentable {
        let content: Content
    
        init(@ViewBuilder content: () -> Content) {
            self.content = content()
        }
    
        func makeUIViewController(context: Context) -> UIViewController {
            let controller = UIHostingController(rootView: content)
            controller.modalPresentationStyle = .fullScreen
            return LandscapeViewController(rootViewController: controller)
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
    }
    
    class LandscapeViewController: UIViewController {
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            return .landscape
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
            scene?.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight), errorHandler: nil)
        }
    }
    
    1. Use this view as a wrapper for your landscape-specific content:
    struct ContentView: View {
        @State private var isLandscapeViewPresented = false
    
        var body: some View {
            Button("Open Landscape View") {
                isLandscapeViewPresented.toggle()
            }
            .fullScreenCover(isPresented: $isLandscapeViewPresented) {
                LandscapeOnlyView {
                    Text("This view is locked in landscape orientation")
                        .padding()
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .background(Color.blue)
                        .foregroundColor(.white)
                }
            }
        }
    }
    

    This ensures that the LandscapeView locks into the landscape orientation before it appears, addressing the issue you described.

    Login or Signup to reply.
  2. This is difficult to do in general. SwiftUI doesn’t directly respect the supportedInterfaceOrientations of UIViewControllerRepresentable view controllers, but you can (kind of) fool it into doing so by presenting another full-screen UIViewController from your representable controller. You’ll have to remove some of your requestGeometryUpdate calls as well. Something like this:

    class LockOrientationContainer: UIViewController {
    
        private let vcToPresent: UIViewController
    
        init(vcToPresent: UIViewController) {
            self.vcToPresent = vcToPresent
            super.init(nibName: nil, bundle: nil)
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func viewWillAppear(_ animated: Bool) {
            if !vcToPresent.isBeingPresented {
                vcToPresent.modalPresentationStyle = .fullScreen
                present(vcToPresent, animated: false)
            }
        }
    }
    
    class LandscapeHostingController<Content: View>: UIHostingController<Content> {
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            return .landscape
        }
    }
    
    struct LandscapeOnlyView<Content: View>: UIViewControllerRepresentable {
    
        let content: Content
    
        init(@ViewBuilder content: () -> Content) {
            self.content = content()
        }
    
        func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
    
    
        func makeUIViewController(context: Context) -> some UIViewController {
            // The `supportedInterfaceOrientations` of the view controller returned from this function will not be respected, 
            // but if that view controller presents another full-screen view controller with its own `supportedInterfaceOrientations`, 
            // those will be respected 
            let vc = LockOrientationContainer(vcToPresent: LandscapeHostingController(rootView: content))
            return vc
        }
    }
    

    This will have a small delay before presenting the content, but it will at least get rid of the unwanted rotation animation.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search