iOS: Disable Autorotation for a Subview
Asked Answered
H

3

19

I have a nested view hierarchy for an iPad application that supports orientation changes. It looks similiar to the following.

UIViewController
    UIView
        - UIView
            - UIImageView (disable rotation)
            - UIImageView
            - UIView (disable rotation)
        - UIView
        - UIView
        ...

I would like to lock the orientation for some of my subviews, while allowing others to auto-rotate and resize. I can't quite seem to figure out how to accomplish this.

One approach seems to be rotating the subviews manually within willAnimateRotationToInterfaceOrientation:. That's not particularly attractive given the SDK is executing a rotation that I would just be undoing.

Is there a way to simply disable orientation changes for subviews or some other method to restructure my hierarchy?

Highclass answered 8/9, 2011 at 20:3 Comment(1)
A simple solution: https://mcmap.net/q/152889/-disable-orientation-change-rotation-animationSouvaine
P
22

Autorotation is handled by a view's UIViewController (shouldAutorotateToInterfaceOrientation:), so one approach is to arrange your hierarchy such that rotatable views are managed by one view controller, and non-rotatable views by another view controller. Both of these UIViewController's root views then need adding to the window/superview.

The subtlety here is that if you have two view controller's views on the same level (i.e. added via addSubview:), only the first view controller (usually the window's rootViewController) will receive the shouldAutorotateToInterfaceOrientation: message.

I used this approach myself to achieve a toolbar that rotates, while the main view does not.

Apple's Technical Q&A QA1688 ("Why won't my UIViewController rotate with the device?") talks a little bit about this issue.


Update for iOS 6:

Autorotation now uses UIViewController's shouldAutorotate and supportedInterfaceOrientations methods. shouldAutorotate returns YES by default, but remember that a view controller other than the rootViewController whose view is a direct subview of the window will NOT receive rotation callbacks anyway.


Sample Code for iOS 6:

Create a new project using the "Single View Application" template, and ensure "Use Storyboards" is checked. We'll use the provided ViewController class as the rotating view controller (rename it if you like!), and create a second UIViewController subclass called NonRotatingViewController. Although this view controller will never even receive the rotation callbacks, for completeness and clarity add the following code in NonRotatingViewController.m:

- (BOOL)shouldAutorotate
{
    return NO;
}

In the MainStoryboard file, drag out a new view controller object and set its class to NonRotatingViewController, and set its Storyboard ID to "NonRotatingVC". While you're there, change the rotating view controller view's background color to clear (the non rotating view will be added underneath this one), and add a label to each view. In AppDelegate.m, add the following code:

#import "NonRotatingViewController.h"

// ...
// ...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
    NonRotatingViewController *nonRotatingVC = [mainStoryboard instantiateViewControllerWithIdentifier:@"NonRotatingVC"];
    [self.window addSubview:nonRotatingVC.view];
    return YES;
}

This is just instantiating a non rotating view controller and adding its view directly to the window (N.B. at this point the window's rootViewController has already been set by the storyboard).

Run the project. Rotate the device and marvel at the sight of one label rotating while the other stays still!


Sample Code pre iOS 6:

I did this in a new project - a new View-based Application will do just fine. Add two new view controllers: RotatingViewController and NonRotatingViewController. Inside each of their nibs I just added a label to describe whether the view should rotate or not. Add the following code:

'RotatingViewController.m'

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


'NonRotatingViewController.m'

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    if (interfaceOrientation == UIInterfaceOrientationPortrait) {    // Or whatever orientation it will be presented in.
        return YES;
    }
    return NO;
}


'AppDelegate.m'

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    RotatingViewController *rotating = [[RotatingViewController alloc] initWithNibName:@"RotatingViewController" bundle:nil];
    self.rotatingViewController = rotating;
    [rotating release];

    NonRotatingViewController *nonRotating = [[NonRotatingViewController alloc] initWithNibName:@"NonRotatingViewController" bundle:nil];
    self.nonRotatingViewController = nonRotating;
    [nonRotating release];

    [self.window addSubview:self.rotatingViewController.view];
    [self.window insertSubview:self.nonRotatingViewController.view belowSubview:self.rotatingViewController.view];

    [self.window makeKeyAndVisible];

    return YES;
}

I hope this helps.

Planish answered 8/9, 2011 at 20:21 Comment(12)
This approach doesn't seem to be working for me. I've configured a view hierarchy with a parent view controller that contains two child view controllers (portraitViewController and rotateViewController), each containing a UIImageView with shouldAutorotateToInterfaceOrientation returning NO for portrait and YES for rotate. If I set shouldAutorotateToInterfaceOrientation to NO in my parent, orientation is static, as expected. Setting shouldAutorotateToInterfaceOrientation to YES in my parent, however, causes both to rotate. Any chance you could post some code? I've gone wrong somewhere.Highclass
@Jason: View Controllers can be quite temperamental and particular - using a custom 'container' controller as you have described might be what is causing the problem. I would ensure that your child view controller views are both attached directly to the application window.Planish
Thanks StuDev. +1 all around. The 'container' controller is definitely causing the problem.Highclass
thank you, this is tricky stuff and this solution worked perfectly for me. i'd like to also add that separating the views means my rotatable view needs to pass through touch events... this answer helped with that: #7381655Abib
In iOS 6, you’ll probably need to set the root view controller. This worked for me: self.window.rootViewController = self.rotatingViewController; Then you call [self.window insertSubview: self.nonRotatingViewController.view belowSubview:self.rotatingViewController.view]; (In my case - yours may be different.)Charlatanism
This doesn't seem correct. Where do you set self.window.rootViewController? You have two viewControllers but neither are set as your app's rootViewController.Sialkot
@mahboudz: An interesting point - I'm guessing that Apple's project templates have changed, and that the root view controller was set in interface builder (I don't have a way of checking this though). The view controller that needs to receive the rotation callbacks does indeed need to be set as the window's rootViewController. I'll update the answer to provide an up-to-date working example.Planish
I guess I'm having a hard time understanding how this works. First off, inserting nonRotatingViewController's view into the window's view hierarchy, doesn't actually activate the nonRotatingViewController, and just places that view under the control of the window, which doesn't do things like autorotation. In effect, you could have used any view that you created and load in the same way and the nonRotatingViewController is a no-op. Can you tell me where I am mistaken?Sialkot
@mahboudz: I'm not sure what you mean by "activate" the view controller. A view controller manages a hierarchy of views, and this is true of the nonRotatingViewController -- it still manages the loading & presentation of its view (viewDidLoad, viewWillAppear etc) and other controller tasks such as presenting additional view controllers. By adding its view directly to the window, all we are doing is preventing nonRotatingViewController from receiving rotation methods. On rotation, the window only adjusts the frame & rotation of the rootViewController.Planish
@mahboudz: While it would be possible to achieve something similar by adding any UIView instance as a subview of the window, you should not do so, as every view should be managed by a UIViewController (either as the view controller's root view, or as part of a hierarchy whose root view is managed by a view controller). Perhaps where you are getting confused is in assuming that the UIWindow does the job of a view controller - UIWindow is a subclass of UIView, hence you can add subviews to it. NonRotatingViewController's view is still managed by the NonRotatingViewController.Planish
I think that in iOS 8 they really rotate the window, so, to be stable in the rotating world, your view should rotate in the opposite direction. The question becomes: "Is any really fixed view in such new world?"Baruch
FYI, I solved the problem creating 2 windows: one rotating and second - fixed. Each window has its own root view controller - one fixed, another - rotating :-)Baruch
S
10

Here is another way. Just place this code into your viewController viewDidLoad:

    YourAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
        // the view you don't want rotated (there could be a heierarchy of views here):
    UIView *nonRotatingView = [[UIView alloc] initWithFrame:CGRectMake(100,0,30,500)];
    nonRotatingView.backgroundColor = [UIColor purpleColor];

        // make sure self.view and its children are transparent so you can see through to this view that will not be rotated behind self.view.

    [delegate.window insertSubview:nonRotatingView  belowSubview:self.view];

        // you can't put it in the front using:
        //    [delegate.window insertSubview:nonRotatingView aboveSubview:self.view];
        // It will show up the same as before

    // you should declare nonRotatingView as a property so you can easily access it for removal, etc.
Sialkot answered 18/1, 2013 at 22:16 Comment(4)
This is working perfectly for me! Simpler than the other solution.Tamas
Someone needs to explain why this works because the code doesn't make it obviousLatrena
The nonRotatingView is being placed between the app's window and the ViewControllers view and is not managed by the viewcontroller. The viewcontroller will handle orientation changes for its views only, and will ignore this view that was placed between it and the window.Sialkot
I have implemented this as I have a AVCaptureSession in my View Controller which I wish should not rotate but I want the non rotating view to be full width and height as my View Controller self.view as I am opening my camera but When I make it the full size the buttons on my self.view of the View Controller are not visible any idea how I can make them visible while keeping the non rotating view full screenMattland
S
4

My approach of solving this problem is doing a countermeasure using UINotification to detect auto rotation and rotating the view the other way round.

Find the complete code here:
https://gist.github.com/ffraenz/5945301

- (void)orientationDidChangeNotificationReceived:(NSNotification *)notification
{
    // find out needed compass rotation
    //  (as a countermeasure to auto rotation)
    float rotation = 0.0;

    if (self.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
        rotation = M_PI;
    else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft)
        rotation = M_PI / 2.0;
    else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight)
        rotation = M_PI / (- 2.0);

    // rotate compass without auto rotation animation
    //  iOS rotation animation duration is 0.3
    //  this excludes the compassView from the auto rotation
    //  (same effect as in the camera app where controls do rotate and camera viewport don't)
    [UIView animateWithDuration:0.3 animations:^(void) {
        self.compassView.transform = CGAffineTransformMakeRotation(rotation);
    }];
}
Saeger answered 7/7, 2013 at 23:17 Comment(1)
This works fine for iOS7 but for some reason, it doesn't work on iOS 8Tot

© 2022 - 2024 — McMap. All rights reserved.