iPhone Auto-rotation for intermediate developers – Tutorial 1

Sure, you’ve been developing on iPhone for 6 months now, but do you *really* understand auto-rotation?

Ever seen strange edge-cases where it doesn’t quite seem to behave as expected?

Ever wondered why UINavigationController and UITabBarController seem to “break” auto-rotation of your custom views – views that work fine if you copy/paste them into a fresh XCode project?

In this post, I’ll cover just the very basics: making a non-rotating app rotate, and examining all the side-effects – and some of the workarounds for them. For each numbered section, you can download the pre-made XCode template for the project where the changes have been done for you. Or you can follow the instructions by hand and see what happens (but it’s easier to download the templates and play with them to see what’s going on).

1. Empty project

iPhone Rotation tutorial A1

Create a “Window-based application”

Do nothing, except put text into the main view so that you can see whether the UIWindow contents are rotated. Also, give it a green background, so we always know where it is (once we later add views on top of it).

Plus, select the UIWindow object in IB, and set the background to “red”. This WILL NOT SHOW through, because that label we just added is taking up the full screen size … but it will become important later (trust me).

Launch and attempt to rotate: screen remains identical, status bar is still at top, so it’s now out of place

2. Add a simple ViewController that always rotates

iPhone Rotation tutorial A2

What happens next is slightly illogical and hard to debug: you add a class that is *referenced nowhere*, yet … a method in that class affects all the other parts of the app.

i. Create an empty VC, and add the following method:

-(BOOL) shouldAutorotateToInterfaceOrientation:
			(UIInterfaceOrientation)toInterfaceOrientation
{
	return TRUE;
}

…we also make the VC’s main view 50% alpha (so we can see what’s underneath it), and add text (so we can see where it is, and which orientation its in).

(for future reference, but don’t worry about it for now: the UIView itself, and the label within it, both have white backgrounds, each applied at 50%. Where both overlap, you get something like 75% white, where only one overlaps you get 50% white)

Also, we remove the “simulated status bar” in the NIB file (all we do is uncheck that option – no other changes), because this VC may need to be added in various places, not just as a “root” VC … and because we’re not even sure if we’ll be using a statusbar all the time. Note that Apple automatically resizes the view to be 460 pixels high.

ii. Ignoring the VC, take the UIView inside it, and add that UIView to the UIWindow:

- (BOOL)application:(UIApplication *)application
	 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{    
	// Override point for customization after application launch.
	[self.window makeKeyAndVisible];
	
	VCThatRotates* rotater = [[VCThatRotates alloc] initWithNibName:nil bundle:nil];
	[self.window addSubview:rotater.view];
	return YES;
}

Things to note

Note #1: VC is never referenced or used as delegate – but Apple finds it anyway

The VC isn’t referenced anywhere – we have a temporary ref that vanishes as soon as the app launches. Yet … the methods in that VC are being accessed by the rest of the system.

Note #2: Not all UIView’s are the same – it’s magic

The UIWindow already had a sub-UIView (an instance of UILabel), and yet adding this second view did something magical which the first UIView did not do. NB: this means that “A UIView” and “A UIView that is referenced by a UIViewController, *specifically* via the named @property “view”” are in fact two different things and interact very differently with the rest of iOS. Again, this is illogical, and leads to massive confusion when trying to debug UIKit issues later on.

Note #3: Pink strips

When we rotate, we see a thin PINK (two variations, both both are shades-of-pink) strip along the sides of the screen. That’s the UIWindow background showing-through. WTF? Didn’t we add the UILabel to the UIWindow in version 1, and didn’t we make it full-screen, and didn’t we say “always resize to fill 100% of the screen”?

NO. Interface Builder made it *look* like we did, but we didn’t. Even Apple realised that the basic design of UIKit was unusable, so they added various hacks to hide their mistake (see the next point for more detail on this). One of those hacks is that when we added the UILabel, it was *not* full screen. The “fix” to make that UILabel fullscreen will *also* break the UILabel whenever the app is in upright position: you literally cannot win. Apple’s combination of design mistakes + hacks mean there’s no easy way out. e.g. If you remove the simulated StatusBar, *and* you manually resize the UIWindow to 480 pixels high (which is the true height – IB was lieing to you), then the top of the label will be underneath the statusbar when you run the app. ARGH!

Note #4: Pink strips are different shades of pink?

The pink strip on left side of screen is a lighter pink that the one on right side of screen. This is because when we created an empty UIViewController, IB automatically made the UIView inside it “autoresize to fill full width/height”, whereas when we added the UILabel into that view, IB automatically made it “attempt to stay the same size”.

Note #5: Green strip at bottom

You’ll see a full-green strip at the bottom. This is where the contents of the UIWindow (i.e. the label we added to the window itself) is peeping-out underneath the UIView/UIVIewController that we added on top. Huh?

…we just hit a design bug in Apple’s iOS that Apple will never fix, because too many people have written workarounds already, and it would break all those apps. The bug is that when you launch the app, the added view doesn’t take up the full screen. Even though it’s exactly the right size/height (460 pixels high).

Why? Well … technically, the Status Bar on iOS is a separate “window” to the application window (which is clever – this way they ensure that you can never “overlap” the status bar. Nice idea, and a good solution to that problem). However … the main window *should* be tiled with the statusbar window, so that (0,0) is the first visible pixel on screen. That’s the sensible way to do it – and Apple’s own UIKit classes are internally coded to emulate this “should” behaviour. Seems that even Apple staff wish it worked that way ;). Instead, someone at Apple placed the main window *underneath* the status-bar window.

(the only trivial way around this flaw that I know of is to disable the status-bar entirely. Ever wondered why some of your favourite apps remove the statusbar, unnecessarily? Sometimes this is the cause: the developer tried to make it work right, gave up, and decided they were wasting too much time and effort debugging their workarounds for Apple’s mistake)

NB: the workarounds also contribute to a bug that many iPhone apps have today: the app doesn’t resize correctly when you receive a phone call. IMHO, the fact that 50,000 developers find this so hard to get right is a signifier that Apple probably chose the wrong approach.

Note #6: 360 degree rotation != 0 degree rotation

When we rotate the screen 360 degrees, we see something different than we started the app. This should be impossible, no? That full-green strip at the bottom has disappeared – and the text for the added View is now aligning with the text embedded in the Window. Magically, the added view (the one in the VC we just created) is moving/resizing to fit the true screen-height. As you can see, the “top” of the uilabel has been shifted down so that it no longer underlaps the statusbar. It appears to have been shifted down by the 20 pixels of the statusbar.

I believe this is a side-effect of Apple’s current “auto-rotate” source-code: yet again, Apple has added a hack to workaround this horrible mistake they made with the StatusBar Window overlapping the main Window. That’s great, but … it’s very confusing for developers. Worse, there are several valid ways to “fix” this – but some of them will have the initial startup screen correct, then Apple’s hack will BREAK the app as soon as someone rotates the screen.

(in my experience, this is one of the most common problems developers hit with auto-rotate: just google for variants of “strip of white when rotating” (the default UIWindow background is white, so for most people, if they shift the view into place, and Apple’s hack then fires and shifts it a second time on rotate, the app appears to gain a strip-of-white at the top)

3. Start adding workarounds

iPhone Rotation tutorial A3

NB: what I’m doing now is (allegedly) what Apple does in their own sourcecode: UINavigationController, UITabBarController etc have code almost identical to this inside them to achieve the same workaround. This is why those Apple classes don’t go horribly wrong (most of the time).

First, where we add our UIView (and accidentally add its parent, “magic”, UIVIewController), we add a workaround for the fact we *know* that there is this “underlapping UIWindow” problem:

CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
CGRect newViewsFrame = rotater.view.frame;
newViewsFrame.origin = CGPointMake(
			newViewsFrame.origin.x,
			newViewsFrame.origin.y + statusBarFrame.size.height );
rotater.view.frame = newViewsFrame;

(Note that instead of hard-coding “origin.y + 20″ we use the reported statusbar height)

Things to note

This time, the initial view is correct, and when you rotate 360 degrees, it looks exactly the same as when you started

4. See how “robust” our workaround is

iPhone Rotation tutorial A4

Very little changes in this version … and yet we discover that Apple’s hacks built-in to Interface Builder *do not work* in common situations. ARGH!

All we do is edit the info.plist for the project, and add the flag:

Status bar is initially hidden = true (NB: the non-GUI-friendly tag saved in plist file is “UIStatusBarHidden”)

Things to note

Oh. Interesting. The UIView that *we* created, and where we did our workaround in the previous version … is now *correctly* displaying aligned with the top of the screen. Apple’s API is reporting that the statusbar frame has a width of 0 and height of 0 – so our code above automagically accomodates this.

But Apple’s code fails. The UILabel we created in Interface Builder now has a 20pixel wide strip at the top where it’s 20 pixels lower down the screen than it ought to be.

(it’s the same strip we saw in version 2, coloured red, but which only appeared in the left, right, and upside-down orientations)

5. Final *fixed* version of Apple’s [UIView addSubview:UIView]

iPhone Rotation tutorial A5

Firstly, from here on in we’re adding a new “iPhone Developers rule”, that applies to graphic designers as well – anyone working with Interface Builder:

Never add anything to a UIWindow, except for a UIViewController.view – don’t even add a UIView!

(c.f. above for the fact that Apple secretly has two types of UIView, due to their own hacks – not all UIView instances are created equal ;) )

Secondly, where we add our UIView (and accidentally add its parent, “magic”, UIVIewController), we will now also do a size-based workaround for resizing issues:

CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
CGRect newViewsFrame = rotater.view.frame;
newViewsFrame.origin = CGPointMake( 0, 0 + statusBarFrame.size.height);
newViewsFrame.size = CGSizeMake(
			window.frame.size.width,
			window.frame.size.height - statusBarFrame.size.height );
rotater.view.frame = newViewsFrame;
[self.window addSubview:rotater.view];

To summarise, what we are replacing is Apple’s

  1. [window addSubview: (myViewController).view];

with:

  1. Find out the height of the status-bar
  2. Take (myViewController).view, and position it at (0, height-of-status-bar) – i.e. immediately butting up against, but NOT under-lapping, the status bar
  3. Take (myViewController).view, and resize it to fit 100% of the remaining available space – NB: this is NOT “window.frame” (because that’s usually-but-not-always larger than the available space)
  4. [window addSubview: (myViewController).view];

…again, as far as I’m aware, this is what Apple does themselves in their own (private) source code for classes like UINavigationController.

Episode 2…

In the next post, I’m going to add the dreaded UINavigationController. You think it’s over-complicated now? Just wait till you see what happens when Apple’s other hacks start kicking-in ;)…

8 thoughts on “iPhone Auto-rotation for intermediate developers – Tutorial 1

  1. Thanks for the post. It is good to know that I am not crazy :>)

    I deal with the 20 pixels essentially the same way, but I fake IB into dealing with it for me.

    I do this by using a container view that is 480 and a root view that is 460 inside it. I think that this ContainerView is only ever used by IB in order to determine that RootView needs to be offset by 20 and pegged to the bottom.

    I have some added complexity because I use a custom tab controller. This means that most of the app needs to run in a view that is 50 pixels shorter than IB likes to think it is if using a standard project.

    Anyway, here’s what I do in IB that works:

    Window – 480 pixels high (autosize struts: top,left)
    RootControllerContainerView – 480 pixels high (autosize struts: top,left)
    RootViewController.view 460 pixels high at y=20 (autosize struts: all EXCEPT top)
    NavControllerContainerView y=0 (any size, but 460 if want to use rest of screen), full autosizing

    I add navigationController.view as a subview to NavControllerContainerView in the code. One would think that would be enough for all of the resizing to work, but navigationController.view ignores autosizing. So, I also set navigationController.view.frame to navControllerContainerView.frame

    Then anything I push onto the nav stack has the correct size, autorotating works, and I can make navControllerContainerView as small as I want to accommodate my customer tab controller, and/or an iAd or any other app-scoped component. If I explicitly change the NavControllerContainerView’s size (ie. show or hide iAd) I need to change navController.view’s size too. Autorotation doesn’t require this.

    So, the code is this:

    viewDidLoad
    ===========
    [navControllerContainerView addSubView:navigationController.view];
    navigationController.view.frame = navControllerContainerview.frame;
    navigationController.view.bounds = navControllerContainerview.bounds;

    and for some unknown reason:

    – (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
    {
    [navigationController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration] ;

    }
    – (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
    {
    [navigationController didRotateFromInterfaceOrientation:fromInterfaceOrientation];
    }

    Anyway, that’s what I do. It works (until it doesn’t).

  2. Lost my indentation on the last comment. Please note that the IB setup has navControllerContainerView as a subView or RootViewController.view which is a subview RootViewControllerContainerView (with nothing inside window).

  3. Rich, is there a reason why you have a container for both the root and the nav? Would it work if I just put the nav controller in a nav container? (window->nav container view->nav view)

  4. HAI……
    I WRITE DEFINATION FOR SHOULD AUTOROTATION YES….
    THE STATUS BAR IS RORATATING BUT NOT VIEW IS I NEED TO DEFINE ANY DELEGATES FOR AUTOROTATION…..
    THANKS
    REGARDS
    SRINIVAS

  5. I wanted to get rid of UINavigationController and came to this problem, your workaround works and saved me lot of hokus-pokus time to find out what the heck status bar is doing… big thanks

  6. I’m running iOS 4.3. I’m not observing that this solves the problem for orientations other than portrait. The view is not resizing. I first tried setting:
    window.view.autresizesSubViews = YES;
    rotater.viewautoresizingMase = 0xFFFFFFFF; //flexible everything

    with no apparent effect.

    I then tried to resize the view in willRotateToInterfaceOrientation: toInterfaceOrientation :duration

    and then sync orientation animation with the modified frames via animateWithDuration:duration delay: options: animations: completion:

    that is all within willRotateToInterfaceOrientation::

    Now I observe rotater.view aligns with inside of red strip.

    If you peg the newFrame.origin at 5,5, you’ll see that it shifts the view as if the coordinate system always assumes portrait mode. That being said, I thought it would be trivial to force align with observed left and bottom status bar. Still, the size of the view does not change after the rotation. I tried setting the frame size and origin in didRotateFromInterfaceOrientation:

    with no observed effect. It’s as if there’s something that auto constrains the width and height.

    That being said, anyone tried got through this?

  7. Bring on the UINavigationController post. It’s screwing up my autorotate suppression code.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>