skip to Main Content

Before iOS 16 presenting a single screen in landscape is fine for portrait application. The working code is as below.

Remark: Whole application is in Portrait mode only.

override public var shouldAutorotate: Bool {
    return false
}

override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return .landscapeLeft
}

override public var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
    return .landscapeLeft
}

I found the solution but it’s for UIWindowScene but I need solution in UIWindow. I need help to fix it in iOS 16.

Xcode – 14.0,
iOS – 16.0,
Simulator – 14 Pro

I can prepare demo if anyone needs.

5

Answers


  1. Chosen as BEST ANSWER

    After many tries I came up with the simple solution. As I mentioned in the question my whole app is in portrait mode only and only one screen that I want to present in the landscape.

    This code doesn't required any external window to be makeKeyAndVisible. If you use extra window to present then you need to write to dismiss separately for iOS 16.

    Old code which was working in previous versions of iOS 16 will be remain same and no change in it.

    Magic lines are as below.

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        if let _ = window?.rootViewController?.presentedViewController as? LandscapeChartVC {
            if #available(iOS 16.0, *) {
                return .landscapeLeft
            } else {
                return .portrait
            }
        } else {
            return .portrait
        }
    }
    

    I've identified my landscape view controller in the appDelegate's supportedInterfaceOrientationsFor.

    Well you can change word presentedViewController to get your controller. And that's it.

    Add support with iPad for all 3 or 4 orientation with this:

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        if UIDevice.IS_IPAD {
            return .allButUpsideDown
        } else {
            if let _ = window?.rootViewController?.presentedViewController as? LandscapeChartVC {
                if #available(iOS 16.0, *) {
                    return .landscapeLeft
                } else {
                    return .portrait
                }
            } else {
                return .portrait
            }
        }
    

    If requirement for iPad app to lock the orientations you can follow iPhone / above code.

    This idea come up with answers and thank you everyone who shared interest. If anyone still get more improved solution I'll happy to update.


  2. I found some related things in iOS 16 Release Notes.
    https://developer.apple.com/documentation/ios-ipados-release-notes/ios-16-release-notes?changes=lat__8_1
    There are some deprecations in UIKit:

    Deprecations
    [UIViewController shouldAutorotate] has been deprecated is no longer supported. [UIViewController attemptRotationToDeviceOrientation] has been deprecated and replaced with [UIViewController setNeedsUpdateOfSupportedInterfaceOrientations].
    Workaround: Apps relying on shouldAutorotate should reflect their preferences using the view controllers supportedInterfaceOrientations. If the supported orientations change, use `-[UIViewController setNeedsUpdateOfSupportedInterface

    I think you may have to use setNeedsUpdateOfSupportedInterface.

    Login or Signup to reply.
  3. this can be done in two ways.

    1 .personally I prefer this way

    1.1 keep this function in AppDelegate to handle the orientation(this is must)

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return .all
    }
    

    1.2 in which ViewController you want the force orientation, go to that view controller and add these lines in the variable declaring section

    var forceLandscape: Bool = false
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
               forceLandscape ? .landscape : .portrait
    }
    

    we will be updating the forceLandscape so it will get updated, then the supportedInterfaceOrientations also will get updated

    1.3 Here we are setting the trigger for updating the forceLandscape (we can add these lines of code inside button action for handling IOS 16 force roatation)

    if #available(iOS 16.0, *) {
                self.forceLandscape = true
                guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
                self.setNeedsUpdateOfSupportedInterfaceOrientations()
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
                    windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight)){
                            error in
                            print(error)
                            print(windowScene.effectiveGeometry)
                    }
                })
    

    this will update the forceLandscape, so it will check the orientation and update according to it

    Above lines of code is for one way, another way is given below

    2. Another way is updating orientation in AppDelegate class:

    2.1 keep this function and property in AppDelegate to handle the orientation(this is must)

    var orientation : UIInterfaceOrientationMask = .portrait
            func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
                return orientation
            }
    

    2.2 In the viewcontroller button action we can update the propperty

    @IBAction func buttonAction(_ sender: Any) {
            let appDel = UIApplication.shared.delegate as! AppDelegate
            appDel.orientation = .landscape
    
            if #available(iOS 16.0, *) {
                DispatchQueue.main.async {
                    let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
                    self.setNeedsUpdateOfSupportedInterfaceOrientations()
                    self.navigationController?.setNeedsUpdateOfSupportedInterfaceOrientations()
                    windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape)) { error in
                        print(error)
                        print(windowScene?.effectiveGeometry ?? "")
                    }
                }
            }else{
                UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
            }
    
    Login or Signup to reply.
  4. The best workaround for presenting modal view controller from portrait in landscape I managed to come up with is combination of setNeedsUpdateOfSupportedInterfaceOrientations(), requestGeometryUpdate(.iOS(interfaceOrientations: .landscape)) and allowed interface orientation on AppDelegate on a new window.

    AppDelegate:

    var allowedOrientation: UIInterfaceOrientationMask = .allButUpsideDown
    
    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return allowedOrientation
    }
    

    Presenting view controller in landscape

    var overlayWindow: UIWindow? // New window to present the controller in
    …
    
    func presentModalInLandscape(vc: ViewController) {
        if #available(iOS 16.0, *) {
            let appdelegate = UIApplication.shared.delegate as! AppDelegate
            appDelegate.allowedOrientation = .landscapeRight
    
            if let currentWindowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
                overlayWindow = UIWindow(windowScene: currentWindowScene)
            }
    
            overlayWindow?.windowLevel = UIWindow.Level.alert
            overlayWindow?.rootViewController = livevc
    
            overlayWindow?.makeKeyAndVisible()
            // It's important to do it after the interface has enough time to rotate
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
               self.overlayWindow?.windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight))
               vc.setNeedsUpdateOfSupportedInterfaceOrientations()
            }
    
        } else {
            // For iOS 15 it's enough to present it modally
            self.present(vc, animated: true, completion: nil)
        }
    }
    
    

    Then when you want to dismiss it you need to

    
    if #available(iOS 16.0, *) {
        self.overlayWindow?.isHidden = true // Destroy the window
        self.overlayWindow?.windowScene = nil
        self.overlayWindow = nil 
        appDelegate().allowedOrientation = .allButUpsideDown // Reset allowed orientation
        self.setNeedsUpdateOfSupportedInterfaceOrientations() // Set the controller back
    } else {
        self.presentedViewController?.dismiss(animated: true)
    }
    

    It’s still not 100% because the view controller is presented in landscape, then flicks back to portrait, and then again rotates to landscape after a second. But without the UIWindow it sometimes does this 2x before it locks in the landscape mode.

    Login or Signup to reply.
  5. The ViewController I want to lock is inside a UINaviationController. For this scenario here’s my working solution

    I have this struct which has a lock and unlock method.

    
    struct AppOrientation {
        
        // statusBarOrientation
        // 0 - unknown
        // 1 - portrait
        // 2 - portraitUpsideDown
        // 3 - landscapeLeft
        // 4 - landscapeRight
        
        
        static let ORIENTATION_KEY: String = "orientation"
        
        private static func lockInterfaceOrientation(_ orientation: UIInterfaceOrientationMask) {
            
            if let delegate = UIApplication.shared.delegate as? AppDelegate {
                delegate.orientationLock = orientation
            }
        }
            
    //    Important
    //    Notice that UIDeviceOrientation.landscapeRight is assigned to UIInterfaceOrientation.landscapeLeft
    //    and UIDeviceOrientation.landscapeLeft is assigned to UIInterfaceOrientation.landscapeRight.
    //    The reason for this is that rotating the device requires rotating the content in the opposite direction.
        
        // Note that UIInterfaceOrientationLandscapeLeft is equal to UIDeviceOrientationLandscapeRight (and vice versa).
        // This is because rotating the device to the left requires rotating the content to the right.
        
        // DevieOrientation
        // 0 - unknown
        // 1 - portrait
        // 2 - portraitUpsideDown
        // 3 - landscapeLeft
        // 4 - landscapeRight
        // 5 - faceUp
        // 6 - faceDown
        
        // UIInterfaceOrientation
        // - landscapeLeft:
        // -- Home button on the left
        // - landscapeRight:
        // -- Home button on the right
        
        // UIDevice orientation
        // - landscapeLeft:
        // -- home button on the right
        // - landscapeRight:
        // -- home button on the left
        
        static func lockDeviceToLandscapeLeft() {
            
            if #available(iOS 16.0, *) {
                let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
                
                if let controller = windowScene?.keyWindow?.rootViewController as? RMNavigationController {
                    controller.lockLandscape = true
                }
            } else {
                UIDevice.current.setValue(UIDeviceOrientation.landscapeLeft.rawValue, forKey: ORIENTATION_KEY)
                lockInterfaceOrientation(.landscapeRight)
            }
            
    
            
            // the docs say you should call 'attemptRorationToDeviceOrientation()
            // lots of StackOverflow answers don't use it,
            // but a couple say you _must_ call it.
            // for me, not calling it seems to work...
    //        UINavigationController.attemptRotationToDeviceOrientation()
        }
        
        static func unlockOrientation() {
            if #available(iOS 16.0, *) {
                let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
                
                if let controller = windowScene?.keyWindow?.rootViewController as? RMNavigationController {
                    controller.lockLandscape = false
                }
            } else {
                lockInterfaceOrientation(.all)
            }
        }
    }
    
    

    Prior to ios16 you only needed to call these two methods.

    As of ios16 you now need to call setNeedsUpdateOfSupportedInterfaceOrientations() as well

    
    override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
      AppOrientation.lockDeviceToLandscapeLeft()
            
      if #available(iOS 16.0, *) {
        self.setNeedsUpdateOfSupportedInterfaceOrientations()
      } else {
        // Fallback on earlier versions
      }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        AppOrientation.unlockOrientation()
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search