Andy Heydon

Tag: modal

iOS6 rotation addendum

I wrote previously about handling rotation in iOS6, notably with respect to modal dialogs which I determined are best handled as child view controllers. I recently extended the app that brought to light a situation I wasn’t handling and therefore I need to add an addendum to that post.

The new feature I added was a full screen modal that I did not want to be rotated on the iPhone. The layout was primarily data entry and hence a keyboard would over a significant portion of the screen, so the flow really dictated a portrait only display. I had to tweak the code to prohibit the rotation on this particular view controller.

A very quick reading of the documentation would suggest that adding the following code to the modal view controller would do the job.

We are saying that this view only supports portrait on the iPhone and that we should not auto-rotate. However this does not work as expected, because under iOS6 container view controllers (such as UINavigationController) do not consult their children about rotation events. The root view controller application in my app is a UINavigationController and so the determination as to rotate or not does not go deeper into the view hierarchy and therefore the autoRotate and supportInterfaceOrientations methods on my modal view controller are never called. I am using a base UINavigationController so the rotation methods fall back to their defaults, which on the iPhone is to allow everything except upside down portrait.

The first step is to force the UINavigationController consult its top level view controller about rotation, which is easily managed by a category:

My top level view controller is also a container that presents modals as child view controllers, so it needs to consult its children too.

So finally this code will ask the modal view controller about rotation and will receive an appropriate answer. This new modal will prohibit rotation, but other simpler and smaller modals still can be rotated.


iOS6 and Rotation Whack-a-Mole

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:

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.

Select Meal TypeIn 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.

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.