How to constrain autorotation to a single orientation for some views, while allowing all orientations on others?
O

5

31

This question is about iOS device rotation and multiple controlled views in a UINavigationController. Some views should be constrained to portrait orientation, and some should autorotate freely. If you try and create the simplest setup with three views, you'll notice that the autorotation behavior has a few very nasty quirks. The scenario is, however, very simple, so I think I'm either not doing the autorotation implementation correctly, or I'm forgetting something.

I have a very basic demo app that shows the weirdness, and I made a video showing it in action.

The setup is very basic: Three view controllers called FirstViewController, SecondViewController and ThirdViewController all extend an AbstractViewController that shows a label with the class' name and that return YES for shouldAutorotateToInterfaceOrientation: when the device is in portrait orientation. The SecondViewController overrides the this method to allow for all rotations. All three concrete classes add a few colored squares to be able to navigate between the views by pushing and popping the controllers onto/off the UINavigationController. So far a very simple scenario, I would say.

If you hold the device in portrait or landscape orientation, this is the result I would not only like to achieve, but would also expect. In the first image you see that all views are 'upright', and in the second you see that only the second view controller counter-rotates the device's orientation. To be clear, it should be possible to navigate from the second view in landscape mode to the third, but because that third only supports portrait orientation, it should only be shown in portrait orientation. The easiest way to see if the results are alright, is by looking at the position of the carrier bar.

Expected view orientation for the device in portrait mode Expected view orientation for the device in landscape mode

But this question is here because the actual result is completely different. Depending on what view you're at when you rotate the device, and depending on what view you navigate to next, the views will not rotate (to be specific, the didOrientFromInterfaceOrientation: method is never called). If you're in landscape on the second and navigate to the third, it will have the same orientation as the second (=bad). If you navigate from the second back to the first however, the screen will rotate into a 'forced' portrait mode, and the carrier bar will be at the physical top of the device, regardless of how you're holding it. The video shows this in more detail.

Actual view orientation for the device in landscape mode

My question is twofold:

  1. Why does the first view controller rotate back, but not the third?
  2. What needs to be done to get the correct behavior from your views when you only want some views to autorotate, but not others?

Cheers, EP.

EDIT: As a last resort before putting a bounty on it, I completely rewrote this question to be shorter, clearer and hopefully more inviting to give an answer.

Obtrude answered 17/2, 2011 at 14:36 Comment(3)
Must be UIWindow thing, if you try to put modal dialog with fixed orientation and switch to a different one the underlying base UIViewController changes its orientation just fine.Obediah
I don't quite get what you mean, there are no modal dialogs involved. Could you elaborate a little?Obtrude
I think modals are implemented by layering UIWindows. UIWindow has another main purpose namely delegating events to enclosed UIViews(in your case UIViewControllers). If for whatever reason your event is being ignored by the key window, its still gets processed by others UIWindows. It's just an observation.Obediah
R
8

The short answer is that you're using UINavigationController, and that won't work like you want it to. From Apple's docs:

Why won't my UIViewController rotate with the device?

All child view controllers in your UITabBarController or UINavigationController do not agree on a common orientation set.

To make sure that all your child view controllers rotate correctly, you must implement shouldAutorotateToInterfaceOrientation for each view controller representing each tab or navigation level. Each must agree on the same orientation for that rotate to occur. That is, they all should return YES for the same orientation positions.

You can read more about view rotation issues here.

You'll have to roll your own view/controller stack management for what you want to do.

Rafaelof answered 22/2, 2011 at 5:9 Comment(8)
Thanks for the quote from the developer site @younce. That makes for a short answer indeed. I can't believe this is the way they restricted it. I guess I'll go with the hacked version then, apart from the lack of animation it works pretty well.Obtrude
@Obtrude You're welcome. I think what you want to do is still achievable, but you'll have to build a custom containing view controller to do it. See the parts about peer view controllers having issues in the link I posted. It seems that you could get around that limitation with some effort.Rafaelof
@Obtrude Also, the hack you posted looks like it uses a private/undocumented API. Are you not planning on submitting to the App Store?Rafaelof
@younce we submitted it today :/ let's see what the review board thinks of this solution. The app won't crash if they change the API, it will just not rotate the intended way anymore.Obtrude
@Obtrude Good luck then. If this closes out your question would you mind accepting the answer? Thanks.Rafaelof
It doesn't close out my question yet, for the hack actually works. I still have a few days on the bounty though, so let's see if there's anyone with a more elegant solution to before killing it with the 'it can't be done' resolution.Obtrude
You win, the app got rejected because of the private API. For the next version, I'll contemplate not using the NavigationController but something else...Obtrude
For anyone watching this, this is no longer a perfect answer since shouldAutorotateToInterfaceOrientation is deprecated. Probably use supportedInterfaceOrientations instead.Amphitheater
M
5

Make a bolean in App delegate to control which orientation you want for example make a bool to enable Portrait and in your view controller you want to allow Portrait enable it by shared application

in your view controller,where you want to enable or disable what ever orientation you want.

((APPNAMEAppDelegate *)[[UIApplication sharedApplication] delegate]).enablePortrait= NO;

in App Delegate.

- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
    NSLog(@"Interface orientations");
    if(!enablePortrait)
        return UIInterfaceOrientationMaskLandscape;
    return UIInterfaceOrientationMaskLandscape|UIInterfaceOrientationMaskPortrait;
}

These method will be fired each time you rotate the device, Based on these BOOL enable the orientation you want.

Motherhood answered 4/12, 2012 at 9:12 Comment(1)
URW ;) I don't know y apple changed something like thisMotherhood
H
4

There was a similar question a few years ago with a number of answers. Here is a recent answer from someone to that question:
Is there a documented way to set the iPhone orientation?

From what I understand, this is a problem a lot of people have and mostly hacks are the only way to fix it. Look through that thread if you haven't seen it before and see if anything works for you.

On a side note, I had a similar problem a while back when I was calling something in shouldAutorotate and I added some code to viewWillAppear to try to fix it. I honestly can't remember if it worked and I don't have a Mac anymore to try it out, but I found the code and I'll paste it here in case it gives any inspiration.

- (void)viewWillAppear:(BOOL)animated{
  UIInterfaceOrientation o;
  switch ([UIDevice currentDevice].orientation) {
    case UIDeviceOrientationPortrait:
        o = UIInterfaceOrientationPortrait;
        break;
    case UIDeviceOrientationLandscapeLeft:
        o = UIInterfaceOrientationLandscapeLeft;
        break;
    case UIDeviceOrientationLandscapeRight:
        o = UIInterfaceOrientationLandscapeRight;
        break;
    default:
        break;
  }

  [self shouldAutorotateToInterfaceOrientation:o];
}
Hardpressed answered 20/2, 2011 at 7:38 Comment(3)
Hey @B! Thanks for the reply man, I thought this was an easy thing that everybody has to deal with, but it looks like I hit something more evasive than I though. Your code example does not work for me, but the link to the other post is very helpful!Obtrude
In short, this won't work. shouldAutorotateToInterfaceOrientation is meant to be called by the framework, and not manually. It does nothing but return a boolean value that the framework uses to inform view rotation.Rafaelof
When I had written this, I had code in shouldAutorotate that modified the interface based on orientation. I don't know if that was the best way to do things but it's what I did at the time.Hardpressed
O
2

DO NOT USE THIS HACK, APPLE WILL REJECT THE APP BASED ON THE USE OF 'PRIVATE API'

For the sake of reference, I will leave my answer here, but the use of private API will not slip past the review board. I learnt something today :D As @younce quoted the Apple docs correctly, what I want cannot be achieved with the UINavigationController.

I had two options. First, I could have written my own navigation controller substitute, with all the horrors that one would have encountered while doing it. Secondly, I could have hacked the rotation into the view controllers, using an undocumented feature of UIDevice called setOrientation:animated:

Because the second was temptingly easy, I went for that one. This is what I did. You'll need a category to suppress compiler warnings about the setter not existing:

@interface UIDevice (UndocumentedFeatures) 
-(void)setOrientation:(UIInterfaceOrientation)orientation animated:(BOOL)animated;
-(void)setOrientation:(UIInterfaceOrientation)orientation;
@end

Then you need to check for the supported orientations on viewWillAppear:. Next to the UIDevice methods used here, you could also force portrait orientation by presenting a modal view controller, but that will happen instantly and not animated, so this is my preferred way:

-(void)viewWillAppear:(BOOL)animated {
    UIDevice *device = [UIDevice currentDevice];
    UIDeviceOrientation realOrientation = device.orientation;

    if ([self shouldAutorotateToInterfaceOrientation:realOrientation]) {
        if (realOrientation != [UIApplication sharedApplication].statusBarOrientation) {

            // Resetting the orientation will trigger the application to rotate
            if ([device respondsToSelector:@selector(setOrientation:animated:)]) {
                [device setOrientation:realOrientation animated:animated];
            } else {
                // Yes if Apple changes the implementation of this undocumented setter,
                // we're back to square one.
            }
        }
    } else if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationPortrait]) {
        if ([device respondsToSelector:@selector(setOrientation:animated:)]) {

            // Then set the desired orientation
            [device setOrientation:UIDeviceOrientationPortrait animated:animated];

            // And set the real orientation back, we don't want to truly mess with the iPhone's balance system.
            // Because the view does not rotate on this orientation, it won't influence the app visually.
            [device setOrientation:realOrientation animated:animated];
        }
    }
}

The trick is to always keep the internal device orientation to the 'real' orientation of the device. If you start changing that, the rotation of your app will be out of balance.

But as I know now, this is just a sure way to get your app rejected. So option number two is just a bad option. Rewrite that NavigationController, or just have all your views support the same orientation set.

Cheers, EP.

Obtrude answered 20/2, 2011 at 12:15 Comment(0)
S
2

In iOS 6, this has become a very simple issue. Simply create a special class for the views you want to autorotate. Then, in your rootVC, add this.

-(BOOL)shouldAutorotate{
       BOOL should = NO;

       NSLog(@"%@", [self.viewControllers[self.viewControllers.count-1] class]);
       if ([self.viewControllers[self.viewControllers.count-1] isKindOfClass:[YourAutorotatingClass class]]) {
             should = YES;
       }


       return should;
}

I know this is an old question, but I thought it worthwhile to mention.

Sigurd answered 10/2, 2013 at 21:51 Comment(3)
Can you please elaborate? What I don't understand is what is RootViewController. I've a storyboard app with 3 scenes. I want only the first scene to be displayed in only in Portrait mode. Adding this code snippet in its class' .m file didn't change anything. What should I do?Aunt
If you have a navigation controller in storyboard (the controller that swaps out one view for another), you need to create a controller class for it, link it up, and then place this code in THAT controller. It's either a UINavigationController or a UITabBarController. If you don't have one, you should, so drag and drop one in storyboard.Sigurd
Yeah I kind of played around and figured it out. Thanks btw :)Aunt

© 2022 - 2024 — McMap. All rights reserved.