4

This is a common question on StackOverflow, but none of the other solutions worked. Many were also written several years ago.

Here are some of the posts considered:

We have several view controllers embedded inside a UINavigationController: A, B, C, D.

A, B use portrait.

C, D use landscape.

A is the root controller.

Assume B gets pushed onto A. That works since B is portrait. However, when C gets pushed onto B, the screen doesn't rotate since as the class docs state:

Typically, the system calls this method only on the root view controller of the window or a view controller presented to fill the entire screen; child view controllers use the portion of the window provided for them by their parent view controller and no longer participate directly in decisions about what rotations are supported.

So overriding supportedInterfaceOrientations inside a custom UINavigationController doesn't help because it isn't consulted on transitions within embedded controllers.

Effectively, we need a way to force orientation changes upon transitions, but there seems to be no supported method for this.

Here's how we override UINavigationController (extension is only used now for debugging purposes since apparently extensions shouldn't be used for overriding):

extension UINavigationController {
    override open var shouldAutorotate: Bool {
        return true
    }

    override open var supportedInterfaceOrientations : UIInterfaceOrientationMask {
        return visibleViewController?.supportedInterfaceOrientations ?? UIInterfaceOrientationMask.landscapeRight
    }
}

Within embedded view controllers, we try to set the orientation like this:

override var shouldAutorotate: Bool {
    return true
}


override var preferredInterfaceOrientationForPresentation : UIInterfaceOrientation {
    return UIInterfaceOrientation.landscapeRight
}


override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.landscapeRight
}

To summarize, the goals are:

1) Show view controllers embedded inside a UINavigationController with different orientations.

2) VC transitions should yield proper orientation change (e.g., popping from C->B should yield portrait, popping from D->C should yield landscape, pushing from B->C should yield landscape, pushing from A->B should yield portrait).

If it were possible to force the UINavigationController into an orientation (with publicly supported method), one possible solution could be to force the orientation upon showing the new view controller. But this also doesn't seem possible.

Suggestions?

Community
  • 1
  • 1
Crashalot
  • 33,605
  • 61
  • 269
  • 439

1 Answers1

4

Step 1

Subclass UINavigationController.

class LandscapeNavigationController: UINavigationController {
    public var vertical: Bool = true

    override var shouldAutorotate: Bool {
        get { return true }}

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        get { return (vertical) ? .portrait : .landscapeLeft }}

    override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
        get { return (vertical) ? .portrait : .landscapeLeft }}
}

Step 2

Use different UINavigationController for different orientations. Yes, pushing a new UINavigationController atop a previous UINavigationController will essentially be modal, but the transition looks nice.

Storyboard with 2 nav controllers

For added convenience, use User Defined Runtime Attributes to control the orientation of the LandscapeNavigationController.

Runtime attributes

Step 3

Add a pop method to handle Back buttons on the now modal UIViewController.

@IBAction func doBack(_ sender: UIBarButtonItem) {
    if let navigationController = navigationController {
        navigationController.dismiss(animated: true, completion: {
        })
    }
}

In Action

Notice how the Top and Bottom labels on view C are properly laid out.

Animation
↻ replay animation


► Find this solution on GitHub and additional details on Swift Recipes.

SwiftArchitect
  • 47,376
  • 28
  • 140
  • 179
  • We wound up doing this right after posting the bounty, but will grant you the answer but there is an issue even presenting, where the second UINavigationController glitches if you leave it in portrait. Here's the code to present: ` let nc = storyboard!.instantiateViewController(withIdentifier: "NavigationControllerID2") as! UINavigationController let vc = nc.topViewController as! TestViewController vc.data = data present(nc, animated: true, completion: nil)` – Crashalot Jan 05 '17 at 16:27
  • I see. I have added a link to a Swift 3 project for you to experiment with. Note that allowing the user to change orientation and preemptively imposing an orientation may lead to unpredictable consequences. – SwiftArchitect Jan 05 '17 at 18:55
  • Sorry, auto correct. Meant there was an issue "with presenting," not "even presenting." Do you not have this glitch (works fine the first time, but subsequent times it glitches)? – Crashalot Jan 05 '17 at 19:02
  • The glitches seem related to this error: `_BSMachError: port 7837; (os/kern) invalid capability (0x14) "Unable to insert COPY_SEND"` Googling and checking SO isn't yielding a solution that works, either. – Crashalot Jan 05 '17 at 19:15
  • Do you mean in the log? Or a visual glitch? Using the project I linked to, and Storyboard segues for navigation, nothing of the sort. – SwiftArchitect Jan 05 '17 at 20:42
  • The glitch was related to a camera issue, perhaps related to requesting permissions. Moving the camera code out of `viewDidLoad` fixed the issue and allowed for embedding another UINavigationController. Thanks for your help! – Crashalot Jan 09 '17 at 08:58
  • This example sadly no longer work in iOS14 with Xcode 12.1 – MonkeyBusiness Nov 30 '20 at 04:32