Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save NeilsUltimateLab/21d551126f0f03b11a0154a681a48e71 to your computer and use it in GitHub Desktop.
Save NeilsUltimateLab/21d551126f0f03b11a0154a681a48e71 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.

@hcn1519
Copy link

hcn1519 commented Aug 1, 2019

Nice Solution without SubClass UINavigationController, UINavigationControllerDeleagte.

Thanks. 😀

@DevAndArtist
Copy link

Don't override the UIKit classes like that, this is a huge source of bugs and will be pain in the butt to debug.

Here are some quotes after I discussed this with other folks on Slack:

Swift extensions of ObjC things are ObjC categories, so yes, should behave identically

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html

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.


Just create a custom subclass and use that instead to override these properties.

@NeilsUltimateLab
Copy link
Author

NeilsUltimateLab commented Dec 7, 2019

Don't override the UIKit classes like that, this is a huge source of bugs and will be pain in the butt to debug.

Here are some quotes after I discussed this with other folks on Slack:

Swift extensions of ObjC things are ObjC categories, so yes, should behave identically

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html

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.

Just create a custom subclass and use that instead to override these properties.

Thanks for bringing attention to this important point. I will update this gist according to this.

EDIT:

I have updated the gist.

@Ilsommo97
Copy link

Ilsommo97 commented Oct 31, 2023

I know its quite an old gist, but I'm really struggling to achieve a simple transition between view controllers that are on different orientations. If presenting from a view controller forced to be in portrait mode while having the device oriented in landscape, to another view controller that has its orientation not restricted by any means, the correct transition is achieved only by specifying the modal presentation style .fullScreen (Using any other presentation style, the presented view controller is shown in portrait mode, and then rotates to landscape)
Since I'm implementing custom transitions, I need to specify .custom as presentation style. Any idea on how to solve this problem? I would really appreciate it
( im not using any navigation controller, just presenting the other view controller over the current one)

@NeilsUltimateLab
Copy link
Author

Thank you so much for reading this gist and commenting.

  • If you are using UIPresentationController for your custom transition, you can set the presentationStyle: UIModalPresentationStyle to .fullScreen to inside your UIPresentationController subclass and observe if it solves the issue (and keep your target viewController's presentationStyle to .custom).

    • i.e.
    public final class StyledPresentationController: UIPresentationController {
        ...
      
        public override var presentationStyle: UIModalPresentationStyle {
            .overFullScreen
        }
    }
  • If not using UIPresentationController, Can you share a small reproducible repository to look into it ?

Thanks.

@Ilsommo97
Copy link

In the end I solved it by specifying .fullscreen as modal presentation style without using presentation controllers. It appears that even when defining a transition delegate (for returning custom animated transitioning) you don't have to use .custom, but can stick to .fullscreen which will detect correctly the orientation of the device. Finally, in the animated transitioning class implemented, you have to redefine the frame of the view ( the one you are transitioning to) by simply assigning its frame to the bounds of the container view used for the transition. Thank you for the reply and for the gist, it was really helpful :)

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