Now back to mobile development (next to a million other things) at Kidiyo, I was underwhelmed with the lack of support from the iOS SDK to force different landscapes across an app. In this blog I share a neat solution that works (at least) from iOS 9.0 onward.

TL;DR: Get my Orientation extension and check how to use it.

Most SO answers suggest to override preferredInterfaceOrientationForPresentation, supportedInterfaceOrientations, and shouldAutorotate. These can be defined on UIViewControllers and on UINavigationControllers.

I’d expect to be able to get it working out of the box by simply overriding preferredInterfaceOrientationForPresentation and supportedInterfaceOrientations on my UIViewControllers and then having UINavigationController exposing those values from the visible view controller.

That setup was insufficient.

The problem

Let’s look at the following scenario.

  • A screen A supports all orientations but prefers portrait
  • A screen B only supports landscape
  • A user can go from A to B and back

Setting B’s supported orientations to landscape did not force that screen to be shown in landscape if I’d come from A in portrait mode. Instead, I was kept in portrait in B until I rotated to landscape. From then on I no longer could rotate back to portrait, as intended from the beginning.

Clearly, the SDK didn’t take the lead on forcing the orientation despite being aware what the current view controller supports.

The way to force rotate a screen is by setting the device “orientation” value:

UIDevice.current.setValue(<UIInterfaceOrientation>.value,
                          forKey: "orientation")

Forcing rotation is easy, but there are a few caveats to cover.

Caveats

Note that preferredInterfaceOrientationForPresentation is of type UIInterfaceOrientation and that supportedInterfaceOrientations is of type UIInterfaceOrientationMask. The second defines the allowed range, while the first indicates which one, within that range, is the preferred.

Having this in mind, it is now clear that preferredInterfaceOrientationForPresentation is the value to use when forcing rotation.

It would seem like a good idea to always force the orientation to be what the visible UIViewController defined preferredInterfaceOrientationForPresentation to be.

This approach works when moving from a screen with a broader set of supported orientations than the screen we are moving to. For instance, when moving from A to B in the scenario above.

Such approach taints the user experience. When at the screen B, the user holds the device in landscape mode. By going back to A, the screen gets rotated to portrait, even though A supports landscape.

Solution

The solution, then, is to force rotation only when the screen we are moving to does not support the orientation we are in already.

Unfortunately, UIInterfaceOrientationMask is an enum and not a list, so we can’t just check if it contains a UIInterfaceOrientation, and it doesn’t provide any function to do this out of the box either.

Therefore, we need to extend it:

extension UIInterfaceOrientationMask {
  func supports(_ orientation: UIInterfaceOrientation) -> Bool {
    return (orientation.isLandscape && self.contains(.landscape))
        || (orientation.isPortrait && self.contains(.portrait))
  }

  func misses(_ orientation: UIInterfaceOrientation) -> Bool {
    return !supports(orientation)
  }
}

Now, when moving to a new UIViewController, we can check if the current orientation supports its preferredInterfaceOrientationForPresentation. If it doesn’t, we have to rotate to that preferred orientation:

extension UINavigationController {
  func adjustOrientationIfNeeded() {
   if self.supportedInterfaceOrientations.misses(currentOrientation()) {
     UIDevice.current
       .setValue(self.preferredInterfaceOrientationForPresentation.rawValue,
                 forKey: "orientation")
   }
 }
}

Bringing it all together

Now we have everything to have it working like a charm:

  • The View Controllers define both their preferred and supported and orientations. You can define this in a base class instead of having to define it in every single view controller.
  • The capacity to decide when to the screen needs to be rotated

The last step is to have your UINavigationController implementing the willShow method from UINavigationControllerDelegate calling adjustOrientationIfNeeded.

extension YourNavigationController: UINavigationControllerDelegate {
  func navigationController(_ navigationController: UINavigationController,
                            willShow viewController: UIViewController,
                            animated: Bool)
  {
    adjustOrientationIfNeeded()
  }
}

Et voilá! You can checkout my whole Orientation extension and get done in no time: