Easiest way to support multiple orientations? How do I load a custom NIB when the application is in Landscape?
Asked Answered
T

5

35

I have an application in which I would like to support multiple orientations. I have two .xib files that I want to use, myViewController.xib and myViewControllerLandscape.xib. myViewController.xib exists in project/Resources and myViewControllerLandscape.xib exists in the root project directory.

What I want to do is use a separate NIB (myViewControllerLandscape.xib) for my rotations. I try detecting rotation in viewDidLoad like this:

if((self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft) || (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight))
 {
  NSLog(@"Landscape detected!");
  [self initWithNibName:@"myViewControllerLandscape" bundle:nil];

 }

But I can see in gdb that this isn't executed when the app is started with the device in landscape. The NSLog message doesn't fire. Why is this? What have I done wrong?

Also, if I explicitly put the initWithNibName function call in the viewDidLoad method, that nib is not loaded, and it continues with the myViewController.xib file. What's wrong with my call? Should I specify a bundle?

Thanks!

Time answered 22/3, 2010 at 23:43 Comment(0)
B
40

You have to load one view, then check orientation and load another if needed. You check orientation in shouldAutorotateToInterfaceOrientation: returning yes if you want to rotate.

I use a navigation controller to manage the transition. If I have the portrait view up and the device rotates, I push the landscape view and then pop the landscape view when it return to portrait.

Edit:

I return YES for all orientations in shouldAutorotateToInterfaceOrientation: but will this be called when the app launches? Do you push your view inside of this function?

The orientation constants are not globals you query but rather part of the messages sent the controller by the system. As such, you cannot easily detect orientation before a view controller loads. Instead, you hardwire the app to start in a particular orientation (usually portrait) and then immediately rotate. (See mobile Safari. It always starts in portrait and then rotates to landscape.)

These are the two methods I used to swap out my portrait and landscape views.

All three view controllers have this method:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

The portrait has this:

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {

    if (toInterfaceOrientation==UIInterfaceOrientationLandscapeRight) {
        [self.nav pushViewController:rightLVC animated:NO];
    }
    if (toInterfaceOrientation==UIInterfaceOrientationLandscapeLeft) {
        [self.nav pushViewController:leftLVC animated:NO];
    }
}

Each landscape controller has this:

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {

    if (toInterfaceOrientation==UIInterfaceOrientationPortrait) {
        [self.nav popViewControllerAnimated:NO];
    }

The app starts in portrait. If the orientation of the device is landscape, it pushes the appropriate landscapes. When the device rotates back to portrait, it pops the landscape. To the user it looks like the same view reorganizing itself for a different orientation.

Blown answered 23/3, 2010 at 0:27 Comment(10)
I return YES for all orientations in shouldAutorotateToInterfaceOrientation: but will this be called when the app launches? Do you push your view inside of this function?Time
Do you have 3 view controllers? If so, how do they talk with each other (how can they all access the nav parameter)? I've tried your code but avoided having 3 controllers. I've tried the following in willRotateTo...: if((toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft) || (toInterfaceOrientation == UIInterfaceOrientationLandscapeRight)) { myViewController *newVC = [[myViewController alloc] initWithNibName:@"myViewControllerLandscape" bundle:nil]; [self.nav pushViewController:newVC animated:NO]; } but no dice. Should I create a whole separate landscape view controller?Time
I know this question is old, but I have an additional question/suggestion: when I want to load a new view controller inside of a tabbar when the orientation changes, that code will not work because a tabbar's navigation controller returns nil. Instead, I loaded a new view and set a view programmatically instead of swapping out view controllers. I'm not sure if this is the right way to do this, but it seems to work. Hope it helps someone...Bullhead
If you want to use this with a tabbar, you would make a navigation controller the top controller in a particular tabview instead of the entire tabbar controller. Then it would work as above.Blown
The only problem I see with this is that you said that if the device is in landscape while the next vc is pushed, you will push the appropriate landscape controller. If that is so, and the orientation changes while that pushed landscape vc is active, the willRotateToInterfaceOrientation: duration: implementation will pop not to the portrait version, but to the previous view controller. Am I missing something?Dumbbell
The navigation controller responds instantly to changes in orientation. Whenever you rotate from one landscape to another, you: pop the current landscape, push the portrait, pop the portrait and then push the other landscape. You can't move from one landscape vc to another without first transitioning through the portrait vc. Each landscape vc only knows how to pop itself back to the portrait vc. In turn, the portrait vc always loads the needed landscape vc for the next rotation.Blown
This answer ALMOST works - unfortunately, because it inserts an extra item into the Nav stack, it breaks the UINavigationBar (you get an extra entry). I had to add a bunch of logic to intelligently hide the navigationBar when pushing landscape OR popping to landscape, and unhide it when pushing anything elseMallina
This approach will cause you grief if you're using a nav controller to manage your hierarchy of views. You either have to nest nav controllers, or use a single nav controller in which case you run into the problems that Adam is describing.Spondaic
I found an alternative that currently works perfectly (iOS 5) - see answer below. It might not work for you (I've only tried it in one of my apps so far)Mallina
The solution is perfect only when there is view part.But this won't work properly, If I got one registration screen controller in both orientation.If filling up few files & I rotate my device then it won;t load that filled data onto another view which got oriented since either it gets pushed or pop.so, I didn;t find this one as an ideal solution where user need to enter the values.The values gets lost when device gets rotated.Do you got some alternative to achieve this ?Imitative
M
59

I found a much better way that works independent of the nav controller. Right now, I have this working when embedded in a nav controller, and when NOT embedded (although I'm not using the nav controller right now, so there may be some bug I've not seen - e.g. the PUSH transition animation might go funny, or something)

Two NIBs, using Apple's naming convention. I suspect that in iOS 6 or 7, Apple might add this as a "feature". I'm using it in my apps and it works perfectly:

  1. triggers on WILL rotate, not SHOULD rotate (waits until the rotate anim is about to start)
  2. uses the Apple naming convention for landscape/portrait files (Default.png is Default-landscape.png if you want Apple to auto-load a landscape version)
  3. reloads the new NIB
  4. which resets the self.view - this will AUTOMATICALLY update the display
  5. and then it calls viewDidLoad (Apple will NOT call this for you, if you manually reload a NIB)
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    if( UIInterfaceOrientationIsLandscape(toInterfaceOrientation) )
    {
        [[NSBundle mainBundle] loadNibNamed: [NSString stringWithFormat:@"%@-landscape", NSStringFromClass([self class])]
                                      owner: self
                                    options: nil];
        [self viewDidLoad];
    }
    else
    {
        [[NSBundle mainBundle] loadNibNamed: [NSString stringWithFormat:@"%@", NSStringFromClass([self class])]
                                      owner: self
                                    options: nil];
        [self viewDidLoad];
    }
}
Mallina answered 10/5, 2012 at 12:3 Comment(11)
This is how I have always done orientation changes, having additional view controllers is silly, unnecessary and wasteful of resources.. not to mention what others above pointed out that it leaves a view controller/view in the stack etc.Annals
You just only need to keep im mind to carry about avoiding allocations in your viewDidLoad, because in this case it coz memory leaks.Twila
If you have memory leaks in your viewDidLoad, then ... you already had memory leaks in your viewDidLoad. Perhaps you could insert a "[self viewDidUnload]" call first, if you need to balance your leaks?Mallina
I've found this to be a much better solution than the accepted answer. Different controllers / "invisible" navigation stack items always struck me as hacky. The only caveat is that the nibName property doesn't update when you load a new one. Thanks.Donyadoodad
Thank You Adam ! I got stuck with a storyboard app needing older code that directly loaded its nibs. Nibs not in the storyboard that is. Was at a loss how to get the rotations to happen on the non-storyboard nibs. Your suggestion, as user282171 said, really clarified the old way ( which is clearly still valid ) and solved the issues. The problem is that storyboarding embeds its nib files and you have to get at them by code [vc.storyboard instatiateViewControllerWithIdentifier ... ]; but if the view-nib you need is standalone ...then [insert Adam's solution here] ;))Solo
Hi @Mallina I did what you have said in this answer it loads the landscape view, but the view is rotated by an angle to 90 degrees.. Though i built the view in IB in landscape orientation .. it is not appearing like that .. what have i done wrong .. how should the view be created in Interface builder.Puttergill
@Sakti sounds like you have a bug in your code somewhere else. Without this code, just loading the NIB normally, is the orientation correct? You should open a new stackoverflow question if not.Mallina
@adam i solved it .. i was trying this for modally presented view controller .. which gave a strange behaviour. i added the view controller to navigation controller and presented it which made it work as intended .. ;)Puttergill
Hi @Adam, I'm having the same issue that Sakti had. I used amergin initWithNibNamed to get the first launch to work (my simulator is coming up landscape now) and my landscape is correct. I go to portrait, and it is correct also, but when I go back to landscape I see that the landscape xib loaded, but it itself is rotated 90 degrees. Does this trick no longer work in iOS6. If you want I'll make a new question.Pentamerous
I did post a new question: #17435897Pentamerous
I've tried this solution and found the following issues: 1. On initial load of the VC, you have to select the right nib. 2. On return to this view, you have to check if you're showing the right orientation somehow. I've found this to be difficult to get right.Isolationism
B
40

You have to load one view, then check orientation and load another if needed. You check orientation in shouldAutorotateToInterfaceOrientation: returning yes if you want to rotate.

I use a navigation controller to manage the transition. If I have the portrait view up and the device rotates, I push the landscape view and then pop the landscape view when it return to portrait.

Edit:

I return YES for all orientations in shouldAutorotateToInterfaceOrientation: but will this be called when the app launches? Do you push your view inside of this function?

The orientation constants are not globals you query but rather part of the messages sent the controller by the system. As such, you cannot easily detect orientation before a view controller loads. Instead, you hardwire the app to start in a particular orientation (usually portrait) and then immediately rotate. (See mobile Safari. It always starts in portrait and then rotates to landscape.)

These are the two methods I used to swap out my portrait and landscape views.

All three view controllers have this method:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

The portrait has this:

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {

    if (toInterfaceOrientation==UIInterfaceOrientationLandscapeRight) {
        [self.nav pushViewController:rightLVC animated:NO];
    }
    if (toInterfaceOrientation==UIInterfaceOrientationLandscapeLeft) {
        [self.nav pushViewController:leftLVC animated:NO];
    }
}

Each landscape controller has this:

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {

    if (toInterfaceOrientation==UIInterfaceOrientationPortrait) {
        [self.nav popViewControllerAnimated:NO];
    }

The app starts in portrait. If the orientation of the device is landscape, it pushes the appropriate landscapes. When the device rotates back to portrait, it pops the landscape. To the user it looks like the same view reorganizing itself for a different orientation.

Blown answered 23/3, 2010 at 0:27 Comment(10)
I return YES for all orientations in shouldAutorotateToInterfaceOrientation: but will this be called when the app launches? Do you push your view inside of this function?Time
Do you have 3 view controllers? If so, how do they talk with each other (how can they all access the nav parameter)? I've tried your code but avoided having 3 controllers. I've tried the following in willRotateTo...: if((toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft) || (toInterfaceOrientation == UIInterfaceOrientationLandscapeRight)) { myViewController *newVC = [[myViewController alloc] initWithNibName:@"myViewControllerLandscape" bundle:nil]; [self.nav pushViewController:newVC animated:NO]; } but no dice. Should I create a whole separate landscape view controller?Time
I know this question is old, but I have an additional question/suggestion: when I want to load a new view controller inside of a tabbar when the orientation changes, that code will not work because a tabbar's navigation controller returns nil. Instead, I loaded a new view and set a view programmatically instead of swapping out view controllers. I'm not sure if this is the right way to do this, but it seems to work. Hope it helps someone...Bullhead
If you want to use this with a tabbar, you would make a navigation controller the top controller in a particular tabview instead of the entire tabbar controller. Then it would work as above.Blown
The only problem I see with this is that you said that if the device is in landscape while the next vc is pushed, you will push the appropriate landscape controller. If that is so, and the orientation changes while that pushed landscape vc is active, the willRotateToInterfaceOrientation: duration: implementation will pop not to the portrait version, but to the previous view controller. Am I missing something?Dumbbell
The navigation controller responds instantly to changes in orientation. Whenever you rotate from one landscape to another, you: pop the current landscape, push the portrait, pop the portrait and then push the other landscape. You can't move from one landscape vc to another without first transitioning through the portrait vc. Each landscape vc only knows how to pop itself back to the portrait vc. In turn, the portrait vc always loads the needed landscape vc for the next rotation.Blown
This answer ALMOST works - unfortunately, because it inserts an extra item into the Nav stack, it breaks the UINavigationBar (you get an extra entry). I had to add a bunch of logic to intelligently hide the navigationBar when pushing landscape OR popping to landscape, and unhide it when pushing anything elseMallina
This approach will cause you grief if you're using a nav controller to manage your hierarchy of views. You either have to nest nav controllers, or use a single nav controller in which case you run into the problems that Adam is describing.Spondaic
I found an alternative that currently works perfectly (iOS 5) - see answer below. It might not work for you (I've only tried it in one of my apps so far)Mallina
The solution is perfect only when there is view part.But this won't work properly, If I got one registration screen controller in both orientation.If filling up few files & I rotate my device then it won;t load that filled data onto another view which got oriented since either it gets pushed or pop.so, I didn;t find this one as an ideal solution where user need to enter the values.The values gets lost when device gets rotated.Do you got some alternative to achieve this ?Imitative
B
5

Really like Adam's answer - I modified it slightly to allow for initial loading of nib in either portrait or landscape

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    if( !nibNameOrNil )     nibNameOrNil = [self nibNameRotated:[[UIApplication sharedApplication] statusBarOrientation]];
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    return self;
}

- (NSString*) nibNameRotated:(UIInterfaceOrientation)orientation
{
    if( UIInterfaceOrientationIsLandscape(orientation))     return [NSString stringWithFormat:@"%@-landscape", NSStringFromClass([self class])];
    return [NSString stringWithFormat:@"%@", NSStringFromClass([self class])];
}

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    NSString *nibname = [self nibNameRotated:toInterfaceOrientation];
    [[NSBundle mainBundle] loadNibNamed:nibname owner:self options:nil];
    [self viewDidLoad];
}
Budgie answered 3/3, 2013 at 12:48 Comment(1)
How do you make this work with Storyboards? I can add a new ViewController that has the landscape layout to the storyboard but then how do I load that when I switch to landscape?Lightish
M
1

I like TechZen's accepted answer but as pointed out in Adam's comment it leaves the portrait view controller in the navigation stack when you rotate to landscape. This means tapping the back button in the navigation bar while in landscape will take the user to the portrait view. A coworker and I are trying the approach of manually removing the extra item from the nav stack. It seems to work but I have very little experience with it yet, so I make no promises. Here is sample code:

To go from portrait to landscape:

[[self navigationController] pushViewController:landscapeViewController animated:NO];
[self removeFromNavigationStack:[MyPortraitViewController class]];

To go back to portrait:

[[self navigationController] pushViewController:portraitViewController animated:NO];
[self removeFromNavigationStack:[MyLandscapeViewController class]];

If you don't use animated:NO you may get warnings regarding the state of the navigation.

Helper:

- (void)removeFromNavigationStack:(Class)oldClass {
      UINavigationController *nav = [self navigationController];
      NSMutableArray *newStack = [NSMutableArray array];
      for (UIViewController *vc in nav.viewControllers) {
          if (![vc isMemberOfClass:[oldClass class]]) {
              [newStack addObject: vc];            
          }
      [nav setViewControllers:newStack animated:NO];
  }
Mendelism answered 28/3, 2012 at 23:10 Comment(1)
We ended up throwing out this approach because we saw some quirky behavior in our app and this approach was one of the suspects. I don't honestly know if this approach was truly at fault.Mendelism
P
1

One more choice, TPMultiLayoutViewController.

Private answered 31/7, 2013 at 13:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.