Friday, May 15, 2009

Real-World iPhone Rotation, Part 1

When I researched the techniques for supporting rotation (portrait vs. landscape) in my iPhone app, I found a few tutorials that all did the same thing: Stick a label in the middle of an otherwise unadorned view and override one method.

Simple, yes? Yes. If you're writing a boring application that just has one line of text in the middle of the screen.

My application has a tab bar. It has multiple screens. It has custom views that pop up from the bottom of the screen. It is not well served by the tutorials I found. Here, then, is one part of my tutorial, based on my own experience.

First, the default mode for any UIViewController is to not support rotation. It's easy to change that if you have a custom class that inherits from UIViewController: Override shouldAutorotateToInterfaceOrientation: to return YES. (You can get fancy with the argument passed in and only support certain orientations, but I didn't.)

But what if you're using a UITabBarController? The same idea applies, unfortunately. Make a subclass of UITabBarController and override that method. In Interface Builder, then, set your custom class as the handler. My class, RotatingTabController, does nothing except override that method and return YES. Yay for class bloat! There may be some way to just set a property in a normal tab bar controller and have it magically start handling rotation, but I haven't found it. So a brand-new class that exists solely to override a boolean return value it is.

However, once you've overridden that method for UITabBarController, you don't need to do it for all the views that hang off of it. But you do need to make sure your views behave when rotated.

The rotation animation on the iPhone is snazzy: Views arc around the screen and rearrange themselves. Get too wrapped up in the idea that you're rotating the view, however, and you'll hurt your head. Instead, think of it as resizing the view, since that's actually what it's doing. It's much easier to grok if you imagine going from this:


to this:


If you're using a built-in data view (table view, image view, etc.) as the sole item on the screen, it probably already behaves the way you want. If you're using a custom layout like this one, which includes a UITableView, controls, and buttons, you'll need to use the Size Inspector in Interface Builder. To control positioning, the Size Inspector uses the "springs and struts" metaphor: Views are locked to superviews (or not) based on the "struts" around the edge , and they expand or contract within their superview based on the "springs" on the inside of the rectangle. Click a strut or spring to enable it or disable it.

The first step in reocnfiguring your view is to set up the autosizing for the main UIView that contains all your subviews. This one's easy: Just enable every strut and spring. That ensures that each edge of the view stays a fixed distance from the corresponding edge of the superview (the application window) and changes its width or height as the window resizes.

I set up the UITableView in the picture the same way: Its top edge is a fixed distance from the top edge of the view (to make room for those other controls), and I want it to expand or shrink based on the containing view. It will expand to fill the width of the superview in landscape mode, and it will add height (and shrink its width) in portrait mode.

I wanted the button in the upper left to stay in the upper left, regardless of the orientation. In that case I enabled the strut on the left side and the strut on the top. I disabled every other strut and every spring. That ensures that the button doesn't change its size based on the orientation and that it stays in the upper left. Not surprisingly, I set up the add button in the upper right and the Today/All control next to it to be locked to the upper right corner, again without shrinking or growing.

A little bit of tinkering got all my views to look nice in either rotation. But I add my own custom views to the UIWindow object, and that is simply not working yet. The view pops out of the edge next to the home button, regardless of the phone's orientation, and it resizes in a sloppy way. When I figure that out, I'll post part 2.

3 comments:

  1. Do we really need to subclass UITabBarViewController just for that? :)

    Kindly let us know if you find an alternative solution.

    Thank you very much!

    ReplyDelete
  2. Hi,
    you can use a Category instead. This allows you to override only one method from a class. For example in this tutorial, you could use the following category for your Tab bar controller :

    @implementation UITabBarController (Orientable)
    // Override to allow orientations other than the default portrait orientation.
    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return YES;
    }
    @end

    ReplyDelete
  3. Matt,

    Good point. I hadn't really learned about categories when I first wired this up, and even now I mostly think of them as a way to do private methods and add functionality, rather than overriding existing functionality. I'll definitely keep that in mind!

    Thanks!

    ReplyDelete