How can I rearrange views when autorotating with autolayout?
Asked Answered
B

2

31

I frequently run into this problem where I want my views arranged in one manner for portrait and a somewhat drastically different manner for landscape.

For a simplified example, consider the following:

Portrait:

Landscape:

Notice how in portrait the images are stacked vertically, but in landscape, they are side-by-side?

Of course, I can manually calculate the positions and sizes, but for more complicated views, this quickly becomes complex. Plus, I'd prefer to use Autolayout so there's less device-specific code (if any at all). But that seems like a lot of Autolayout code to write. Is there a way to do set up the constraints for this sort of stuff through the storyboard?

Braden answered 28/5, 2015 at 18:59 Comment(3)
This question deserves an answer regarding size classes...Braden
The best and easiest tutorial I found to solve the exact problem here is from raywenderlich raywenderlich.com/83276/beginning-adaptive-layout-tutorial Note: Seems like it only can be used from iOS 8 aboveJerriejerrilee
Related: In autolayout, how can I have view take up half of the screen, regardless of orientation?Braden
B
35

There are two underused features of Xcode's interface builder we can make use of here to make exactly this sort of thing a synch.

  1. You can create IBOutlet connections for NSLayoutConstraints.
  2. You can hook up "outlet collections", which is an array of IBOutlet objects.

So with these two things in mind, the gist of what we want to do is create all of our autolayout constraints for one orientation, hook them all up in an outlet collection. Now, for each of these constraints, uncheck the "Installed" option on interface builder. Then make all of our outlet collections for another layout, and hook them up to another outlet collection. We can create as many of these layout groups as we want.

It's important to note that we will need a reference to any UI element which has constraints installed on it directly, and we will need a seperate outlet collection not just for each layout we want, but for each UI object which has constraints installed on it directly.

Let's take a look at the fairly simplified example from your question.

enter image description here

If you look in the constraints list on the left, you can see half of them are grayed-out. The grayed-out constraints are the landscape constraints. I created all of these, then unchecked "Installed" for each of them:

enter image description here

Meanwhile, the ungrayed, normal looking constraints are the portrait constraints. For these, I left them "Installed". It's completely unnecessary to leave any of them installed, but you will run into problems if you leave multiple sets installed (they most likely conflict).

enter image description here

Be sure not to check "Remove at build time" for any of these. We don't want the constraints "removed" at build time. This simply means the constraint isn't created at all (so we'll lose the reference to it). If we leave this check marked while we have an IBOutlet to the constraint, Xcode will generate a warning:

Unsupported Configuration
Connection to placeholder constraint. Constraints marked as placeholders in IB should not have any connections since these constraints are not compiled into the document and will not exist at runtime.

Anyway, so now we need to hook up the constraints to an outlet so we can access them at run time.

Hold Ctrl and click and drag from one of the constraints to your source code file, just as you would connect any other UI element. On the dialog that pops up, choose Outlet Collection and a descriptive name:

enter image description here

Now hook up all of the other constraints that match that constraint group into the same outlet collection:

enter image description here

Once we've finished hooking up all of our constraints, it's just a matter of removing/adding them at the appropriate time.

For such a simple example as the scenario described in the question, we can override updateViewConstraints with some code like this:

Swift

class ViewController: UIViewController {
    @IBOutlet var landscapeConstraints: [NSLayoutConstraint]!
    @IBOutlet var portraitConstraints: [NSLayoutConstraint]!

    override func updateViewConstraints() {
        let isPortrait = self.view.bounds.width < self.view.bounds.height

        self.view.removeConstraints(self.portraitConstraints)
        self.view.removeConstraints(self.landscapeConstraints)
        self.view.addConstraints(isPortrait ? self.portraitConstraints : self.landscapeConstraints)

        super.updateViewConstraints()
    }
}

Objective-C

@interface ViewController()

@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *landscapeConstraints;
@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *portraitConstraints;

@end

@implementation ViewController

- (void)updateViewConstraints {
    BOOL isPortrait = self.view.bounds.size.width < self.view.boudns.size.height;

    [self.view removeConstraints:self.portraitConstraints];
    [self.view removeConstraints:self.landscapeConstraints];
    [self.view addConstraints:(isPortrait ? self.portraitConstraints : self.landscapeConstraints)];

    [super updateViewConstraints];
}

@end

We're not checking which set of constraints the view previously had, so just remove both of our constraint sets, and then add the appropriate set we want to use.

This is all the code we need to manage completely changing out an entire set of constraints on an object at run time. This allows to work in the interface builder to set all of our constraints up instead of having to do it programmatically (which I find a little more tedious and error-prone).

The end result? Very nice looking autorotation rearrangement without pulling your hair out getting the autolayout code done perfectly:

Braden answered 28/5, 2015 at 18:59 Comment(1)
This is the epitome of a Stack Overflow answer: a well-explained, elegant solution to an annoying, widely-applicable, and practical but complex task. You've just saved me hours of time, and I'm sure that'll be true of others over the months to come. Bravo for a truly canonical question/answer pair!Justis
C
1

Now that UIStackView is available in iOS9, a simpler and guaranteed solution would be to add both views to a stackview, link the stackview to an IBOutlet and then add the following code to the viewcontroller:

- (void) adjustViewsForOrientation:(UIInterfaceOrientation) orientation {

if(orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown || orientation == UIInterfaceOrientationUnknown)
    self.stackView.axis = UILayoutConstraintAxisVertical;
else
    self.stackView.axis = UILayoutConstraintAxisHorizontal;

[self.stackView setNeedsLayout];
[self.stackView setNeedsDisplay];

}

and call this method from

-(void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
Cosenza answered 1/8, 2016 at 0:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.