iOS6 and Rotation Whack-a-Mole
by Andy
Recently I started a new iOS app primarily to research a few user interaction effects, but it may turn into a real-life app. As a universal app the iPad naturally support all rotations, but on the iPhone I wanted both portrait and landscape views despite the large difference in aspect ratio.
Apple introduced a new way of handling rotation in iOS6, which the documentation covers pretty well. In the long term it should make things simpler, but in the short term developers will need to support the iOS5 way by responding to the shouldAutorotateToInterfaceOrientation:
method, and the iOS6 way by coordinating the Info.plist rotation settings, the application delegate’s application:supportedInterfaceOrientationsForWindow:
and a view controller’s supportedInterfaceOrientations
methods. The easiest way to handle the latter is to create a category for UIViewController:
@implementation UIViewController (Rotation) | |
// iOS 6 | |
- (BOOL)shouldAutorotate { | |
return YES; | |
} | |
- (NSUInteger)supportedInterfaceOrientations { | |
return IPAD ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown; | |
} | |
@end |
These methods indicate which rotations are valid for all views, and indeed work very nicely when you rotate the device. But, and the whole point of this blog post is built around the but, there are a few issues to overcome with iOS6.
In one area of the app, I wanted to present a small modal dialog on top of the current view to allow the user to select an item from a list. A semi-transparent shade layer appears underneath the modal to trap any taps on the screen (taps on this layer cancels the modal much like a UIPopover) but still allow the user to see what is underneath, and the whole presentation is less glaring transition than the standard full screen change, which hides the current context.
The standard iOS way for a view controller to show a modal view is via presentViewController:animated:completion:
. It is relatively easy to insert a semi-transparent view into the presented controller’s view hierarchy, add a gesture recognizer to collect the taps on that background and to show a pretty view on top. But, and here comes that but, when you rotate the device the modal view controller rotates very nicely just as it should, the presenting view controller, the one that is visible underneath your shade doesn’t budge. This because iOS on the iPhone presents modals as full screen and saves some work by not sending rotation events to the view controllers earlier in the stack.
This state of affairs caused much gnashing of teeth – the mismatched text orientation between the two levels looked ugly, but I really wanted the semi-transparent effect to reduce the weight of the user interface. I also wanted to retain the modal in its own view controller because I anticipated using the pattern in several places and I didn’t want to load up the main view controller with a bunch of extra views.
I tried many things, some of them fairly ugly hacks, but in the end there was effectively a single line of code solution.
iOS5 introduced the concept of custom container view controllers. There have always been container view controllers such as UINavigationController and UITabBarController in iOS, but now we can implement our own containers with our own logic for transitioning between views or indeed to present several views concurrently on a screen, each one managed by its own controller.
[self addChildViewController:modalViewController]; | |
[[modalViewController view] setFrame:[[self view] bounds]]; | |
[[self view] addSubview:[modalViewController view]]; | |
[modalViewController didMoveToParentViewController:self]; |
The addChildViewController:
call establishes the relationship between the container and modal, then we say whereabouts the child will appear on the screen so that the child’s view is part of the container’s view hierarchy. Custom containers must call didMoveToParentViewController:
explicitly to tell the child that the transition is complete. The child container can override this method to react to its state change. When you are ready to delete the child view there is a removeChildViewContainer:
method. Containers will automatically forward rotation events to their child controllers if the shouldAutomaticallyForwardRotationMethods
returns YES, which happens to be the default.
Bonus tip: setting the modal’s frame before displaying it is important because it avoids any problems with positioning views during rotation. I didn’t do this in my initial implementation and saw some rather bizarre locations of views after rotation.
So there we are, I have the appearance of a modal laid on top of a view, everything visible rotates perfectly, and the main view and modal views have their own controllers for good code separation.
[…] wrote previously about handling rotation in iOS6, notably with respect to modal dialogs which I determined are best handled as child view […]