Allowing one specific controller inside of UINavigationController to rotate

Time for an iOS related post about a requirement that bugged me in the past and it seems, quite a few people out there have similar issues:

How can I have a UINavigationController based app, where all controllers are forced into portrait but one of them should support both, landscape and portrait?

How hard can it be? Well, not hard, but surprisingly tricky. There are solutions out there for Swift and I used them as a source of inspiration to make this Xamarin.iOS version which you can find on my Github. So what exactly is the problem?

iOS supports landscape and portrait orientations and they can be configured in Info.plist. By default, all controllers will then rotate which is working fine as long as you are using AutoLayout all the way and design your screens with all orientations in mind. In general, an app should not force any orientations because optimally, the app would run on different iPhones and iPads – and all of them have different aspect ratios and resolutions. But there are certainly apps specifically made for iPhone, which are meant to be used in portrait only – period. This means, you would select “portrait” as the only option in Info.plist; unfortunately, then you’re locked into portrait only…

But what if there is one single controller that should support landscape, because you’d like to show additional data and make use of the wider screen? In this case, UIViewControllers allows to override methods to return the allowed and preferred orientations and whether the screen should automatically be rotated.

This works just fine until you introduce a UINavigationController and the controller you’d like to allow landscape for will be pushed onto the stack. The result is that the controller in question can indeed use the methods mentioned above to allow additional orientations, however you can only limit rotation but not completely prevent it. For example: you can prevent rotation of your UINavigationController and all of the controllers on the stack. For one of them you allow landscape. When pushing that controller it will be portrait. Then you rotate the device and you get landscape. However, navigating back to the previous controller will make that one also to be landscape, even if it does not specify it as a valid orientation. Is it a bug? I don’t know – at least it feels wrong. Can we fix it? Yes, we can!

The solution

Three steps are necessary to allow a controller being pushed to support both orientations and reset the orientation back to portrait upon back navigation.

Step one

In Info.plist select all the orientations you would like to support in the controller/controllers that are not portrait-exclusive. Typically, this means selecting portrait, landscape left and landscape right.

Step two

Create a custom UINavigationController class and make sure to specify it in your storyboard if you are using the designer. In the custom code override GetSupportedInterfaceOrientations() which decides the allowed orientations. In there, check if the controller you would like to have landscape support for is the one that is currently on top of the stack. If yes, let it return its supported orientations; otherwise force to portrait.

public partial class CustomNavController : UINavigationController
{
  public CustomNavController (IntPtr handle) : base (handle)
  {
  }

  public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations()
  {
    if (VisibleViewController is PortraitAndLandscapeController)
    {
     return VisibleViewController.GetSupportedInterfaceOrientations();
    }

    return UIInterfaceOrientationMask.Portrait;
  }
}

Step three

Finally, the code for the controller which also supports landscape (in my case PortraitAndLandscapeController) has to return its allowed orientations and in addition perform a small “hack” to force the orientation back to portrait when being dismissed. This is important because otherwise the previous controller on the stack would suddenly go into landscape although it does not explicitly support it.

public override void ViewWillDisappear(bool animated)
{
    base.ViewWillDisappear(animated);

    // Little hack...but this forces orientation back into portrait when leaving the controller.
    if (IsMovingFromParentViewController)
    {
        UIDevice.CurrentDevice.SetValueForKey(NSNumber.FromNInt((int)UIInterfaceOrientation.Portrait), new NSString("orientation"));
    }
}

We force the orientation value of the device back to portrait if the parent view controller (the navigation controller) is dismissing the screen. This value can normally only be read, however by using a lower level API, we are able to also set it. 😈

The result is a behavior as seen in this animation:

Allowing landscape for one controller of the stack

You can find a complete project over on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *