iOS16+ Present UIViewController in landscape only for single screen not working [Swift 5.7]

Viewed 485

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

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.

3 Answers

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.

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")
        }

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.

Related