Skip to content

Instantly share code, notes, and snippets.

  • Save karosLi/b1fcc7d0b75edd20bc17844fcfa26fc9 to your computer and use it in GitHub Desktop.
Save karosLi/b1fcc7d0b75edd20bc17844fcfa26fc9 to your computer and use it in GitHub Desktop.
Understanding UIViewController rotation when embed in Container View Controllers.

Understanding UIViewController Rotation

Problem

To enable the rotation of a single view controller used to display the preview of Images/Videos. It is intuitive to allow user to rotate there device and screen changes accordingly, so it feels pleasant. But to achieve this, we need to enable the (almost) all Supported Device orientations.

Ex: `Portrait`, `LandscapeLeft`, `LandscapeRight`.

By enabling Supported Device orientations either from Info.plist or via AppDelegate.

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

By doing this we are allowing other view controllers to also rotate if device orientation changes. But we want only Image/Video Previewing view controller is allowed to Rotate in all orientations.

Approach

A view controller can override the `supportedInterfaceOrientations`
method to limit the list of supported orientations.

By doing we can allow our view controllers to limited orientations like below:

ViewController

class ViewController: UIViewController {
  ...
  override var shouldAutorotate: Bool {
    return false
  }
    
  override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
      return .portrait
  }
  ...
}

PreviewingViewController

class PreviewingViewController: UIViewController {
  ...
  override var shouldAutorotate: Bool {
    return false
  }
    
  override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
      return .allButUpsideDown
  }
  ...
}

Results

Configuration 1:

The ViewController is now the rootViewController of current window and our PreviewingViewController embedded in UINavigationController is being presented modally covering the entire screen.

  • Result: It worked as desired. 😇

Configuration 2:

The ViewController is embedded in UINavigationController, and PreviewingViewController is the same as Configuration 1.

  • Result: It's not working now. ViewController screen is now rotating again as device orientation changes. 🙁

Configuration 3:

The ViewController is embedded in UITabBarController, and PreviewingViewController is the same as Configuration 1.

  • Result: Same as result as in configuration 2. ☹️

Observations

Lets put break-point at supportedInterfaceOrientations on both ViewController and PreviewingViewController.

In Configuration 1: Break-point being hit every time when new orientation is applied on both view-controllers.

In Configuration 2: Break-point hit for first time, but not when device rotated on both view-controllers.

In Configuration 3: same as configuration 2.

Revising the documentation 🧐

A view controller can override the `supportedInterfaceOrientations`
method to limit the list of supported orientations. 
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; 

Yes, this is why Configuration 1 is working properly. In this configuration we have our ViewController as the only rootViewController of our window.

if let window = (UIApplication.shared.delegate as? AppDelegate)?.window {
  print(window.rootViewController is ViewController) 
}

// Prints true.

And for the PreviewingViewController is being presented covering entire screen, so its supportedInterfaceOrientations property is also called every time device changes to new orientation.

So when the device orientation changes we get called for the appropriate UIInterfaceOrientationMask

####So what's the deal for the Configuration 2?

Yes, now we have UINavigationController as our window's rootViewController.

if let window = (UIApplication.shared.delegate as? AppDelegate)?.window {
  print(window.rootViewController is UINavigationController)
}

// Prints true.

We need to provide appropriate supportedInterfaceOrientations to our UINavigationController controller in order to get notified in ViewController

Lets extend UINavigationController.

extension UINavigationController {
  open override var shouldAutorotate: Bool {
    return true
  }
    
  open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return topViewController?.supportedInterfaceOrientations ?? .allButUpsideDown
  }
}

Now we have told navigationController to ask its topViewController to return appropriate supportedInterfaceOrientations

As we run, we get hit at break point every-time when device is rotated to new orientation.

Now lets see for our Configuration 3 too.

extension UITabBarController {
  open override var shouldAutorotate: Bool {
      return true
  }
    
  open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return selectedViewController?.supportedInterfaceOrientations ?? .allButUpsideDown
  }
}

As we have told UITabBarController to ask its selectedViewController to return its supportedInterfaceOrientations. As expected, this is working too, Break-point hit when device is rotated to new orientation. Hurray. 😎

Question: Why we have to extend UINavigationController or UITabBarController (ContainerViewControllers) to do rotation according to its children?

Further reading the documentation...

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. 

This may be the default implementation for container view controllers.

Important Note

The sole purpose of this gist to understand rotation behaviour of child viewControllers in ContainerViewController like UINavigationController, UITabBarControllers, UISplitViewControllers etc.

To implement proper rotation behaviour in our viewControllers, we should subclass these ContainerViewController and then override these properties, because extending these UIKit Classes globally will cause unexpected behaviour as mensioned in Customizing Existing Classes.

If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined as to which method implementation is used at runtime. This is less likely to be an issue if you’re using categories with your own classes, but can cause problems when using categories to add methods to standard Cocoa or Cocoa Touch classes.

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment