Thursday, 17 December 2009

iPhone: Forcing a UIView to reorientate

In my current iPhone application I need to force a UIView to reorientate when a certain event happens. The SDK doesn't really allow for this to happen.

There's the well-known UIViewController method
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation that is called whenever the user rotates the iPhone. If you say YES to that orientation then the OS does the spade work of rotating the display and your views for you. However, you cannot manually ask to rotate the screen, so if you decide that your shouldAutorotateToInterfaceOrientation: peccadillo has changed, there's no way to tell the OS.

The blind alley

There are a number of blog posts, discussions, and Stack Overflow posts that suggest you use the private setOrientation: method of the UIDevice class. This is all well and good apart from two issues:
  1. It's a private API. You won't get into the App store if you use it.
  2. More importantly, it doesn't work.
This seems the canonical version of that method:
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft];
[[UIDevice currentDevice] setOrientation:UIInterfaceOrientationLandscapeLeft];
It serves to rotate the status bar, but leaves your UIView where it is.

How to make it work

Until Apple bless us with an official API for doing this, here's how I managed to achieve my goal...

Once I have decided that I want to orientate a different way, and arrange to return a different shouldAutorotate answer, you must change your window's subview hierarchy. Doing this forces the OS to ask you about your rotation support and wiggle everything around appropriately.

Based on the fact my app has a parent UINavigationViewController at the top, which puts a single subview into the UIWindow, this is what I do:
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIView *view = [window.subviews objectAtIndex:0];
[view removeFromSuperview];
[window addSubview:view];
It's a bit unpleasant, but it works. I tried variants such as removing the window and then making it the keyWindow again, however that didn't trick the OS into asking for rotation state again.

I hope this helps you. As ever, if you know a better way of doing this, please let me know!

20 comments:

Phil Nash said...

Very interesting! I've been providing my own transform matrices, but this sounds like it could be a lot easier. Will give it a try myself later.
Thanks Pete. We should do a conference presentation together or something. Oh wait

Rodrigo Costa said...

Thanks i was looking all over the internet for this!!!!!!

it worked perfectly !!!

Anonymous said...

Yep it works well. I found it too some days ago :) ( http://www.geckogeek.fr/iphone-forcer-le-mode-landscape-ou-portrait-en-cours-dexecution.html ) (french tutorial with codes)

Anonymous said...

GENIUSSSSSSSSSSSS!!!!!!!!!!!!!!

nobre84 said...

Awesome! Thank you very much, we've been digging and digging for a working solution to this.

nobre84 said...

It seems this method doesn't work for iOS 3.1.2 / 3.1.3 ? :(
Works fine on 4.0+ but I'm not getting the expected results on onder devices. Any workaround available ?

Anonymous said...

Even though this method is called the interfaceOrientation argument has a wrong value :(

Anonymous said...

I'd been scratching my head on this one for a while. Thanks for sharing the info.

Thomas Schuster said...

Thanks for sharing!

ShiFu said...

Thanks for sharing this! I still couldn't get it to work. Where do you put this code? Is it in viewDidLoad or viewWillAppear? Is it possible to share more context?

Here is my problem. I have a navigation controller inside a top level tab bar controller. I need to support one view in landscape mode only and the rest in all orientations.

If I put your code in viewWillAppear, it runs into an infinite loop.

If I put it in viewDidAppear, it works fine for the first time. Then if I rotate the screen in the special landscape view and pop that view, the navigation controller remains the old state and won't auto rotate to the correct orientation. Then when I push the special view again, it will rotate incorrectly.

Pete Goodliffe said...

This definitely does not belong in viewDid/WillAppear!

Just put it in the top-level view controller and invoke it whenever you need to reorient the view.

This, clearly, will not be when the view first appears as you can set the orientation normally in that case; when your view appears for the first time, iOS will ask your view for it's preferred orientation.

This trick is only needed when the view is already on-screen and then needs to reorient itself.

ShiFu said...

Pete,
Thanks a lot for the quick response. Now I understand more about the purpose of your code. It's to rotate the view regardless of the physical orientation of the phone. An alternative to achieve that is below:

UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];

if (orientation == UIDeviceOrientationLandscapeLeft) {
[self.view setTransform:CGAffineTransformMakeRotation(M_PI)];
} else if (orientation == UIDeviceOrientationLandscapeRight) {
[self.view setTransform:CGAffineTransformMakeRotation(0.0)];
}

else if (orientation == UIDeviceOrientationPortraitUpsideDown) {
[self.view setTransform:CGAffineTransformMakeRotation(M_PI/-2.0)];
} else if (orientation == UIDeviceOrientationPortrait) {
[self.view setTransform:CGAffineTransformMakeRotation(M_PI/2.0)];
}

I put that in viewWillAppear.

But I still haven't found a solution to my problem.

Here is my problem again. I have a navigation controller inside a top level tab bar controller. I need to support one view in landscape mode only and the rest in all orientations.

I have tried many approaches from stackoverflow.com but all of them have some major issues.

I have get the special view show up correctly the first time. But if I exit and rotate the phone and reenter, the view will rotate incorrectly.

I think I have to try to replace pushViewController with presentModalViewController.

Any ideas?

Thanks!

Touch said...

finally working solution, just set statusbar orientation, remove and add view in window and thats it.. no transformations etc... not so clean but working

Anubha said...
This comment has been removed by the author.
Anubha said...
This comment has been removed by the author.
can.peskersoy said...

Hi guys, i have an another suggestion which was also implemented in YouTube App. It's basically adding/removing a modal view into/from current rootview which triggers the shouldAutorotateToInterfaceOrientation method. In Youtube App when we click the video link, the active model is dismissed and video appears in landscape mode...

PS: Even you can disable default animation of presenting Modal View and add custom one!

Anonymous said...

The method of adding then removing a modal view is indeed better. The first method has a small problem when the device orientation is locked.

arun.k said...

really helped...was struggling for few days

Anonymous said...

Thanks a lot!!! its working fine...

micmic3 said...

Thank you!Do it in ios 5 is find. But I found setstatusbarorientation in ios 6
is not working...