41
votes

Using interface builder in xcode and just one .xib file, how can I create alternate layouts when rotating between landscape and portrait orientations?

See diagram of differing layouts enter image description here

N.b. the green view/area would contain 3 items flowing horizontal in landscape and in portrait those 3 items would flow vertically within the green view/area.

8
Wasn't my answer enough to solve your problem ?Grzegorz Krukowski
Thanks but technically you didnt answer my question, I asked how it can be done with one nib. Plus there's 5 days left on the bounty. I'll wait and see if other people have solutions. Hold tightDave Haigh
With the introduction of the UIStackView in iOS 9, this kind of layout is very easy. Put the views into stack views, and after the view is loaded, you just flip the orientation of the stack views to either vertical or horizontal depending on the orientation of the screen. The system will take care of all the layout changes for you.Jake
@Jake nice. what happens when you use UIStackView on pre iOS9 devices?Dave Haigh
@DaveHaigh I assume using UIStackView on pre iOS9 devices will crash the app unless you check for availability of the class and provide an alternative implementation for pre iOS9 devicesJake

8 Answers

44
votes

A way to do this is to have three views in your .xib file. The first one is the normal view of your Viewcontroller with no subviews.

Then you create the views for portrait and landscape as you need them. All three have to be root-level views (have a look at the screenshot)

enter image description here

In your Viewcontroller, create 2 IBOutlets, one for the portrait and one for the landscape view and connect them with the corresponding views in the interface builder:

IBOutlet UIView *_portraitView;
IBOutlet UIView *_landscapeView;
UIView *_currentView;

The third view, _currentView is needed to keep track of which one of these views is currently being displayed. Then create a new function like this:

-(void)setUpViewForOrientation:(UIInterfaceOrientation)orientation
{
    [_currentView removeFromSuperview];
    if(UIInterfaceOrientationIsLandscape(orientation))
    {
        [self.view addSubview:_landscapeView];
        _currentView = _landscapeView;
    }
    else
    {
        [self.view addSubview:_portraitView];
        _currentView = _portraitView;
    }
}

You will need to call this function from two different places, first for initialization:

-(void)viewDidLoad
{
    [super viewDidLoad];
    UIInterfaceOrientation interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
    [self setUpViewForOrientation:interfaceOrientation];
}

And second for orientation changes:

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [self setUpViewForOrientation:toInterfaceOrientation];
}

Hope that helps you!

14
votes

There is no automatic way to support that since it is against Apple design. You should have one ViewController supporting both orientations.

But if you really want to do so you need to reload xib's on rotation events and load different nib files.

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [[NSBundle mainBundle] loadNibNamed:[self nibNameForInterfaceOrientation:toInterfaceOrientation] owner:self options:nil];
    [self viewDidLoad];
}

- (NSString*) nibNameForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    NSString *postfix = (UIInterfaceOrientationIsLandscape(interfaceOrientation)) ? @"portrait" : @"landscape";
    return [NSString stringWithFormat:@"%@-%@", NSStringFromClass([self class]), postfix];
}

And you create two nib files post-fixed with "landscape" and "portrait".

8
votes

I like @MeXx's solution, but it has the overhead of keeping two different view hierarchies in memory. Also, if any subview has state (e.g. color) that changes, you'll need to map that across when you switch hierarchies.

Another solution might be to use auto-layout and swap the constraints for each subview for each orientation. This would work best if you have a 1:1 mapping between subviews in both orientations.

Understandably you want to use IB to visually define the layout for each orientation. Under my plan you'd have to do what @MeXx prescribes, but then create a mechanism to store both sets of constraints once the nib was loaded (awakeFromNib) and re-apply the correct set on layout (viewWillLayoutSubviews). You could throw away the secondary view hierarchy once you scraped and stored its constraints. (Since constraints are view-specific you'd likely be creating new constraints to apply to the actual subviews).

Sorry I don't have any code. It's just a plan at this stage.

Final note - this would all be easier with a xib than a storyboard since in a storyboard it's painful to describe views that live outside of a view controller's main view (which is desirable since otherwise its a PITA to edit). Blach!

3
votes

Ya you can sir surely ....

I used to do is by giving frame manually when Device rotates

Whenever device rotates , - (void)viewWillLayoutSubviews get called

for Example - take a UIButton *button in your UIView,

- (void)viewWillLayoutSubviews
   {
       if (UIDeviceOrientationIsLandscape([self.view interfaceOrientation]))
       {
         //x,y as you want
         [ button setFrame:CGRectMake:(x,y,button.width,button.height)];

       }
       else
       {
         //In potrait
          //x,y as you want
         [ button setFrame:CGRectMake:(x,y,button.width,button.height)];


       }

   }

In this way you can place it as you like . Thanks

Please have a look on these images

First image of my xCode XIB UIView which i want in both screen

enter image description here

Now as i want to look this view in landscape too so i went to edit my -(void)viewWillLayoutSubviews

enter image description here

Now the result is like this First image of potrait Screen in Simulator

enter image description here

and this is in Landscape

enter image description here

2
votes

Solution with storyboard [ios7]. Inside the main view controller, there is the container view that can include the tab bar controller. Inside the tab bar controller, there are 2 tabs. One tab is for controller with portrait and another is for controller in landscape mode.

The main view controller implements these functions:

#define INTERFACE_ORIENTATION() ([[UIApplication sharedApplication]statusBarOrientation])

@interface MainViewController ()
@property(nonatomic,weak) UITabBarController *embeddedTabBarController;
@end

@implementation MainViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"embedContainer"])
    {
        self.embeddedTabBarController = segue.destinationViewController;
        [self.embeddedTabBarController.tabBar setHidden:YES];
    }
}

- (void) adaptToOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    if (UIInterfaceOrientationIsLandscape(interfaceOrientation))
    {
        [self.embeddedTabBarController setSelectedIndex:1];
    }
    else
    {
        [self.embeddedTabBarController setSelectedIndex:0];
    }
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration
{
    [self adaptToOrientation:interfaceOrientation];    
}

- (void) viewWillAppear:(BOOL)animated
{
    [self adaptToOrientation:INTERFACE_ORIENTATION()];
}

The segue link in storyboard should be named "embedContainer".

Assure that container view is resizable to fulfil the parent view, in both cases, landscape and portrait.

Take care about the fact that in the runtime 2 instances of the same controller lives in the same time.

Storyboard


Update: Apple docs for alternate landscape view controller

2
votes

I'd just like to add a new answer to reflect upcoming new features in ios8 and XCode 6.

In the latest new software, Apple introduced size classes, which enable the storyboard to intelligently adapt based on what screen size and orientation you are using. Though I suggest you look at the docs above or the WWDC 2014 session building adaptive apps with UIKit, I'll try to paraphrase.

Ensure size classes are enabled,.

You will notice your storyboard files are now square. You can set up the basic interface here. The interface will be intelligently resized for all enabled devices and orientation.

At the bottom of the screen, you will see a button saying yAny xAny. By clicking this, you can modify just one size class, for instance, landscape and portrait.

I do suggest that you read the docs above, but I hope this helps you, as it uses only one storyboard.

1
votes

You can use the library GPOrientation. Here is link

1
votes

Resize Programatically

I have an app where I have exactly the same situation. In my NIB I just haven the portrait layout. The other layout is performed programatically. It is not that difficult to specify a few sizes programmatically, so no need to maintain two NIBs.

Note that performing the view change programatically by setting frames will result in an animation (or can easily be made using an animation). This will give the user a valuable feedback about which component moved to which position. If you just load a second view, you will lose this advantage and from a UI perspective I believe loading an alternative view is not a good solution.

Use two ViewControllers

If you like to use two ViewControllers, then Apple describes how to do it in its developer documentation, i.e., you find the answer to you question here: RespondingtoDeviceOrientationChanges.

If you like to do it programatically you may add re-position code in the method didRotateFromInterfaceOrientation.

Example

I added the method to my ViewController:

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
    [self adjustLayoutToOrientation];
}

Then implemented the adjustment of the layout. Here is an example (the code is specific to my app, but you can read sizes of superviews and calculate frames of subviews from it.

- (void)adjustLayoutToOrientation
{
    UIInterfaceOrientation toInterfaceOrientation = self.interfaceOrientation;
    bool isIPhone = !([[UIDevice currentDevice] respondsToSelector:@selector(userInterfaceIdiom)] && [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad);

    [self adjustViewHeight];
    [self adjustSubviewsToFrame: [mailTemplateBody.superview.superview frame]];

    CGRect pickerViewWrapperFrame = pickerViewWrapper.frame;
    if(isIPhone) {
        pickerViewWrapperFrame.origin.x = 0;
        pickerViewWrapperFrame.size.height = keyboardHeight;
        pickerViewWrapperFrame.origin.y = [self view].frame.size.height-pickerViewWrapperFrame.size.height;
        pickerViewWrapperFrame.size.width = [self view].frame.size.width;
    }
    else {
        pickerViewWrapperFrame = pickerView.frame;
    }

// MORE ADJUSTMENTS HERE

}