Partial View Controller Rotation in iOS6

Yesterday I was working on upgrade one of my applications for iOS6, where I have all UIViewControllers in portrait mode but one, which is allowed to rotate in landscape for text editing.

In iOS5, doing this was not that big problem, you would simply implement shouldAutorotateToInterfaceOrientation method in UIVewController and that was it, but it’s not that simple in iOS6.

In iOS6 release notes you can find this:

Autorotation is changing in iOS 6. In iOS 6, the shouldAutorotateToInterfaceOrientation: method of UIViewController is deprecated. In its place, you should use the supportedInterfaceOrientationsForWindow: and shouldAutorotate methods.

More responsibility is moving to the app and the app delegate. Now, iOS containers (such as UINavigationController) do not consult their children to determine whether they should autorotate. By default, an app and a view controller’s supported interface orientations are set to UIInterfaceOrientationMaskAll for the iPad idiom and UIInterfaceOrientationMaskAllButUpsideDown for the iPhone idiom.

A view controller’s supported interface orientations can change over time—even an app’s supported interface orientations can change over time. The system asks the top-most full-screen view controller (typically the root view controller) for its supported interface orientations whenever the device rotates or whenever a view controller is presented with the full-screen modal presentation style. Moreover, the supported orientations are retrieved only if this view controller returns YES from its shouldAutorotate method. The system intersects the view controller’s supported orientations with the app’s supported orientations (as determined by the Info.plist file or the app delegate’s application:supportedInterfaceOrientationsForWindow: method) to determine whether to rotate.

The system determines whether an orientation is supported by intersecting the value returned by the app’s supportedInterfaceOrientationsForWindow: method with the value returned by the supportedInterfaceOrientations method of the top-most full-screen controller.

The setStatusBarOrientation:animated: method is not deprecated outright. It now works only if the supportedInterfaceOrientations method of the top-most full-screen view controller returns 0. This makes the caller responsible for ensuring that the status bar orientation is consistent.

For compatibility, view controllers that still implement the shouldAutorotateToInterfaceOrientation: method do not get the new autorotation behaviors. (In other words, they do not fall back to using the app, app delegate, or Info.plist file to determine the supported orientations.) Instead, the shouldAutorotateToInterfaceOrientation: method is used to synthesize the information that would be returned by the supportedInterfaceOrientations method.

OK, so this means that we need to subclass UINavigationController, to add shouldAutoRotate and supportedInterfaceOrientations and then implement in every UIViewController its own supportedInterfaceOrientations so that navigation controller can use them.

Let’s get started.

  • Create new iPhone project, you can take Single View Application template. By default all rotations except upside down are allowed.
  • Add one more UIViewController, name it SecondViewController, subclass of UIViewController
  • Open AppDelegate.h and add class CustomNavigationViewController and property to it @property (strong, nonatomic) CustomNavigationViewController *navController;
  • Open AppDelegate.m and add @interface and @implementation for CustomNavigationViewController, we will subclass it latter
1
2
3
4
5
6
7
@interface CustomNavigationViewController : UINavigationController

@end

@implementation CustomNavigationViewController

@end
  • Now we will change rootViewController from view controller to navigation view controller, so change method didFinishLaunchingWithOptions as I did bellow
1
2
3
4
5
6
7
8
9
10
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
    self.navController = [[CustomNavigationViewController alloc] initWithRootViewController:self.viewController];
    self.window.rootViewController = self.navController;
    [self.window makeKeyAndVisible];
    return YES;
}
  • Open ViewController.m file and add following line to viewDidLoad method
1
[self.navigationItem setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"Push the second VC" style:UIBarButtonItemStyleBordered target:self action:@selector(pushTheSecondViewController)]];
  • On the top of the file add #import "SecondViewController.h" so that we can push SecondViewController to navigation controller
  • Add following method
1
2
3
4
- (void)pushTheSecondViewController {
    SecondViewController *svc = [[SecondViewController alloc] initWithNibName:@"SecondViewController" bundle:nil];
    [self.navigationController pushViewController:svc animated:YES];
}

So what we have now is simple iPhone application with 2 view controllers that both can be rotated in those three directions allowed. So far so good, but the problem comes if you want only the second view controller to be able to rotate. Try to rotate iOS simulator with cmd+left/right keys to verify that you can rotate both view controllers in all orientations.

In iOS5 you would simple implement shouldAutorotateToInterfaceOrientation method where you would return UIInterfaceOrientation that you support and controller would respect that.

1
2
3
4
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait ? YES : NO );
}

If we try to do the same approach in iOS6 and implement shouldAutoRotate and supportedInterfaceOrientations methods to the ViewController.m nothing is going to happen.

1
2
3
4
5
6
7
- (BOOL)shouldAutorotate {
    return YES;
}

- (NSInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

As you can see here, code is in place, but it’s not being called. In iOS6 release notes we can find why on this line:

More responsibility is moving to the app and the app delegate. Now, iOS containers (such as UINavigationController) do not consult their children to determine whether they should autorotate. By default, an app and a view controller’s supported interface orientations are set to UIInterfaceOrientationMaskAll for the iPad idiom and UIInterfaceOrientationMaskAllButUpsideDown for the iPhone idiom.

What this mean for us?

It means that we need to add some code to our CustomNavigationControllerView in AppDelegate.m file.

  • open AppDelegate.m file
  • in implementation of CustomNavigationControllerView add following
1
2
3
4
5
6
7
- (BOOL)shouldAutorotate {
    return YES;
}

- (NSInteger)supportedInterfaceOrientations {
    return [self.topViewController supportedInterfaceOrientations];
}
  • build and run

Now you can see that the first view is supporting only portrait mode while the second view is supporting all those modes that application is supporting by default (portrait, landscape left, landscape right). The key ingredient here is this line: return [self.topViewController supportedInterfaceOrientations]; where we are returning those UIInterfaceOrientationMasks that we want to see in our view controller. Even if you push the second view to navigation controller stack and then press back button, the first view controller will rotate in its own supported orientation.

This is how you need to do it in the future if you need this partial view controller rotation functionality in your app.

You can fork working example app from Github repo.

Note 1: shouldAutoRotate has to be set on YES for this to work, because if it is set to NO app won’t check supportedInterfaceOrientations at all

Note 2: don’t forget to implement shouldAutorotateToInterfaceOrientation method in case that you need to support iOS5

Note 3: it’s very important to use self.window.rootViewController in AppDelegate, because without it rotations won’t work

iOS