18 Aug 2009

My story's often told: app meets UITabBarController, UITabBarController starts an affair with multiple UINavigationControllers, the UINavigationControllers end up with a host of child UIViewControllers, some of which need to rotate to landscape mode and some that don't.

So far, so good.

Then, the plot thickens! In comes an MFMailComposeViewComposer that you want to display via presentModalViewController. Your view's in landscape, you tap the button that brings up the modal mail compose screen and – oh, the injustice, the tragedy – it flips your view to portrait mode before displaying itself.

Bummer!

Of course, there's more to the story. You had started rotating your views yourself using CGAffineTransforms when you saw that getting autorotation to work via shouldRotateToInterfaceOrientation for complicated view hierarchies required a four year degree in Voodoo. Now, you feel you may be paying for your insolence for deviating from the One True Path of the Church of the Holy Apple DDFS. (I don't know what DDFS stands for but it makes names cooler when they have random letters after them.)

So you dig deeper. It appears that your view controllers all return UIInterfaceOrientationPortrait when queried for their interfaceOrientation properties, even when the device is in one of the landscape modes. (Note the subtle impedance mismatch between "interface" and "device" in the previous sentence, it's a clue!) Aha, and it appears that either presentModalViewController or MFMailComposeViewController is looking for that value and, being the control freak that it is, actually forcing the layout to that orientation before displaying itself.

Well, there's another control freak in town, buddy, and there ain't room enough for the both of us!

So what's a control freak with a scripting background to do but monkey patch this baby… categories to the rescue! If I can get the interfaceOrientation method to return the actual orientation of the device, instead of the interfaces's orientation, it should work!

@interface UIViewController(OrientationPatch)
-(UIDeviceOrientation)interfaceOrientation;
@end
 
@implementation UIViewController(OrientationPatch)
 
-(UIDeviceOrientation)interfaceOrientation
{
	return [[UIDevice currentDevice] orientation];
}
 

And it does… somewhat. Now the MFMailComposeViewController is displaying properly but reverts the app to portrait when it is dismissed! Doh!

More investigating and it appears that shouldRotateToInterfaceOrientation is now being called on my UITabBarController and, since it wasn't implemented, is now returning NO to everything.

I know how to fix that: subclass UITabBarController and implement shouldRotateToInterfaceOrientation:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
	return YES;
}

Et, voilà, the MFMailComposeViewController now displays perfectly in landscape mode and excuses itself gracefully without blowing down the whole house.

None of this should be considered great advice to follow. Having found shouldRotateToInterfaceOrientation too painful to work with in my view hierarchy, I resorted to rotating my views myself (since I support rotation in just one, clearly defined section of my application, this has so far proven much easier to implement). If you can get things working with shouldRotateToInterfaceOrientation, you should stick to that. If you, however, find yourself in rotation hell like I did, maybe some of the above will make sense to you and lead you to diagnose or even fix the issues you may be encountering.

Regardless, it definitely feels like autorotation support, especially selective autorotation support for complex view hierarchies, is something that the iPhone SDK team could concentrate a bit on for future releases.

Add Your Comment

Spam Protection by WP-SpamFree

iPhone rotation woes and a workaround

  1. Oh my you just saved my last project that I almost binned while trying to get that shouldRotateToInterfaceOrientation to work properly!

    Next year on #2M10 I’m buying all your drinks Aral!

    Kenneth
  2. LOL, that’s great to hear Kenneth; looking forward to seeing your app :)

    Aral
  3. I ran into a very similar thing bringing up address book picker and photo picker. What I found finally worked was calling presentModalViewController from the root view controller. Then it respects the orientation of that view controller. Calling it from subviews/view controllers was causing my problem.

    Keith Peters
  4. Interesting, Keith – does your app also use a UITabBarController or was the UINavigationController’s view added directly to the window?

    Aral
  5. Hi, Aral,
    Can I post two small questions, both related to orientation rotation and MFMailComposeViewController? You seem as though you’ve thought about this a lot!

    First, I have an app with several views under a navigationController, and I allow landscape mode in only one of the views. I use transforms and set up the landscape mode myself rather than allowing autorotation. But I keep the navigationBar and toolbar in portrait mode, like the thumbnails page in iPhone’s Photos app. But I’d like to have a thin navigation bar in landscape mode, like when you’re looking at an individual photo in Photos. I tried transforms, etc., on the navigation bar, but can’t get it to work. Have you gotten a landscape-oriented navigation bar without using shouldRotate?

    Then: I want to use MFMailComposeViewController, because I want a user to be able to send email with photo attachments, as shown in the MailCompose example application.

    However, because of the nature of the app, the destination “To:” of the email will always be the same. And so will the subject. And I don’t want to confuse the user with fields for CC: and BCC:, or to give him access to his Contacts list in that view.

    Can MFMail display without some of those fields? Ideally, I’d like to populate the view’s fields programmatically, and then programmatically click the “Send” button, so the user doesn’t see the form at all. See? Use its functionality, but use *my* UI.

    Can that be done?
    Thanks for any insight!
    /Steve

    Steve Denenberg
  6. Hi Steve,

    In response to your first question, I found that forcing the bars to refresh themselves gets me the shorter versions when switching to landscape and the taller ones when going back. So, in the method where I apply the transforms, I have the following hack:

    [self.navigationController setNavigationBarHidden:YES animated:NO];
    [self.navigationController setToolbarHidden:YES animated:NO];
    [self.navigationController setNavigationBarHidden:NO animated:NO];
    [self.navigationController setToolbarHidden:NO animated:NO];

    As for your second question, you can set the various fields but you cannot affect the MFMailComposeViewController once it displays. This is by design. Apple doesn’t want your app to be able to send email without it being in the control of the user.

    The only way around this that I can see is to build your own custom mail interface and use an SMTP library (or do a post to a web app and send the mail from your server).

    Hope this helps!

    Aral
  7. Hi, Aral,
    Thanks!! Can you post the transform code? My navigation bar is not just thick when I rotate it into landscape, but it’s distorted and unreadable, even though I thought I was just doing a 90 degree rotation. But also, resetting the bar’s frame to place it at the top of the landscaped view doesn’t work either. I think I’m doing something fundamentally wrong, and I’d love to see how someone else does it.

    About mail, I think I’ll just punt and use MFMailCompose until Apple comes out with something more configurable.

    Thanks again.
    /Steve

    Steve Denenberg
  8. Sweet! Thanks! Now I know I’m not going crazy!

    My hack was less elegant… I punched a reference to my TabBarController into the UIViewController that “lives” on one of my tabs from inside TabBarController thus…

    myNewUIViewController.tabBarController = self;
    (set up tabBarController with TabBarController * tabBarController, and synthesize in MyNewUIViewController class)

    I needed a text view to change size based on orientation, but only when edited…

    Thus my non-functional statement querying the state of the orientation with a “self.interfaceOrientation”, which fails as you say…

    Became “myTabBarController.interfaceOrientation” and viola, it worked…

    JohnB
  9. Hi, Aral,
    Don’t know whether you missed my plea above for the navigation bar transform code. I haven’t been able to find working code to do that anywhere, and you seem to have solved the problem.
    Thanks again,
    /Steve

    Steve Denenberg
  10. Hi Aral,
    Thank you for your post and sharing this.
    Would you have a tip to force the iPhone to stay in landscape mode no matter what application it is running? if it is a matter of just changing a system .plist file… (I need it for a temporary use and would switch it back when done…)
    Thanks

    Alin
  11. Overriding the interfaceOrientation method seems drastic & dangerous! My problem was that I have an app that’s primarily portrait-only and with one modal view controller which I want to autorotate, which I got working well, but when I was opening it it would always start in portrait. I found shouldAutorotateToInterfaceOrientation got called with UIInterfaceOrientationPortrait starting out even when phone was landscape for example. The view controller seemed to be preferring the parent view controller’s orientation over the device’s. Is this what your problem was too?

    This is what worked for me:

    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    UIDeviceOrientation deviceOrientation = [[UIDevice currentDevice] orientation];

    // if device orientation is one of these, don’t care what our interface orientation is
    if (deviceOrientation == UIDeviceOrientationUnknown && deviceOrientation == UIDeviceOrientationFaceUp && deviceOrientation == UIDeviceOrientationFaceDown)
    return YES;

    // otherwise only return YES for the ui orientation matching the current device orientation
    return (interfaceOrientation == deviceOrientation);
    }

    smallduck
  12. I tried to poll the Device Orientation, but it so happens that some times it returns things other than LandscapeLeft, LandscapeRight, Portrait and PortraitUpsideDown. The views would autorotate on the slightest tiltings and become inconsistent with the status bar. I guess the Device supports “Facing UP” and other fancy stuff unrelated to the INTERFACE orientation.

    Then I decided that [[UIApplication sharedApplication] statusBarOrientation] is the right place to look at. I always gives you the right answer. In viewWillAppear I check this orientation and call my custom methods (i.e., adjustToPortraitAnimated:NO and adjustToLandscapeAnimated:NO)

    In didAutorotateFrom… I call the same methods with the animation parameter set to YES.

    These methods calculate the new frames for each subview, but NOT based on self.view.frame (which is often bullsh*t), but I instead hard-coded these:

    landscape: (0,0, 480, 268) // Accounts for NavBar Height
    portrait: (0,0, 320, 416) // Accounts for NavBar Height

    I then set the frames right away OR inside an animation block, depending if the animation argument is YES or NO.

    It works for me (Most of the time); hope it helps someone…

    nicolás
  13. If you’re using ‘navigation controllers’ and ‘tab bars’… and want to make a landscape view display. this code is inside the view controller’s viewDidLoad() method:

    [[UIApplication sharedApplication]
    setStatusBarOrientation:UIInterfaceOrientationLandscapeRight
    animated:NO];

    self.navigationController.view.transform = CGAffineTransformMakeRotation(M_PI / 2);

    CGRect contentRect = CGRectMake(0, 0, 480, 320);
    self.navigationController.view.bounds = contentRect;

    [UIView beginAnimations: nil context: nil];
    self.navigationController.view.alpha = 1.0f;
    [UIView commitAnimations];

    Shawn Arney
  14. Aral,

    Great post. I’ve had some luck with my app getting my modal view to support all orientations, but only if the controller that is presenting it is already rotated. When I rotate the device when the modal view is already presented, nothing rotates. Any ideas?

    Erik
  15. You dont need to subclass UITabBarController. You can use categories to get around that.

    .h file should include:

    @interface UITabBarController (Orientable)
    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation;
    @end

    .m should do:

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

    Rohit
  16. Hi,

    I was able to get my UITabBarController to respond to shouldAutorotateToInterfaceOrientation by making sure all of the views it controlled also supported shouldAutorotateToInterfaceOrientation. I read someplace that all the subviews needed to support shouldAutorotateToInterfaceOrientation.

    Marc

    Marc
  17. Hey Aral,

    For the record, I had a similar problem but your solution didn’t seem to work. Not sure why.

    I solved it by presenting the modal view controller from the ‘top’ view controller in the stack rather than the one I was currently on. Your post set me on the right track though!

    Cheers,
    Neil

    Neil
  18. can you provide some sample project or code that i can refer to. I am having same issue but not with Toolbar or anything. I am opening UIVIewController as modal view and when i dismiss my parent view’s orientation get changed to portrait. It works perfectly in portrait mode but this happens only in landscape mode.

    Yash
  19. I am most grateful to you for this.

    Really saved my difficult life of havving several viewcontrollers in a tabbar and using modal.

    Thanks!

    Ariel
  20. Great post… saved me a headache. Thumbs up for the hack–great use of categories!!

    Ilias
  21. Hi friend. Me too have this problem in iPad landscape mode. I lost three full days to find the solution, still i didn’t get. Can you help me…. I have added a ViewControllerA in Window, This is having UISegmentedController in navigationbar. I have added 4 more ViewControllers in UISegmentedController as SubView named as ViewControllers A(Self),B,C,D. In ViewControllerB i want to call another one ViewControllerE by ModalViewController type.

    When i call this code in ViewControllerB,

    ViewControllerE *controller = [[ViewControllerE alloc]init];
    controller.modalTransitionStyle = UIModalPresentationFullScreen;
    [self presentModalViewController:controller animated:YES];

    imodal view show in Portrait mode and it not show fullscreen in portrait also… How to solve it?? Can you help Me??? Please save me… Thanks in advance.. Yuva.M, sorry for my poor english..

    YUVARAJ.M
  22. Hi,

    I solved the same problem simply by calling presentModalFrameController in willAnimateRotationToInterfaceOrientation instead of willRotateToInterfaceOrientation

    Cheers!

    Andrius Bolsaitis
  23. Hi Aral,

    I am having great problem with this mail landscape problem. But still didn’t get any solution. Can you help me a bit in this regard?

    Here i my code snippet. If you want i can send you the total code.

    Thanks in advance..

    -(void) mailCallback: (id) sender {
    //self = [super init];
    NSLog(@”I am in mail callback”);
    [self displayComposerSheet];

    }

    -(void) displayComposerSheet
    {
    NSLog(@”Now I am in the ComposeSheet funtion”);
    NSArray *toRecipients = [NSArray arrayWithObject:@"sajeebcsedu@gmail.com"];
    [[CCDirector sharedDirector] pause];
    [[CCDirector sharedDirector] stopAnimation];
    emailController = [[UIViewController alloc] init];
    [emailController setView:[[CCDirector sharedDirector] openGLView]];
    picker =[[MFMailComposeViewController alloc] init];
    picker.mailComposeDelegate=self;
    [picker setToRecipients:toRecipients];
    [picker setSubject:@"TEST"];
    [picker setMessageBody:@"JAJAJA" isHTML:YES];

    [emailController presentModalViewController:picker animated:YES];
    [picker release];
    }

    -(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
    {
    [[CCDirector sharedDirector] resume];
    [[CCDirector sharedDirector] startAnimation];

    [[CCDirector sharedDirector] replaceScene: [CCTransitionFadeDown transitionWithDuration:1.0f scene:[HelloWorldLayer node]]];
    [controller dismissModalViewControllerAnimated:NO];
    }

    Sajeeb Saha