Can I use autolayout to provide different constraints for landscape and portrait orientations?
Asked Answered
M

6

50

Is it possible to change the constraints when the device is rotated? How might this be achieved?

A simple example might be two images which, in portrait, are stacked one above the other, but in landscape are side-by-side.

If this is not possible, how else might I accomplish this layout?

I am constructing my views and constraints in code and not using interface builder.

Murillo answered 21/7, 2013 at 13:40 Comment(0)
F
41

Edit: Using the new concept of Size Classes introduced in Xcode 6, you can easily setup different constraints for specific size classes in Interface Builder. Most devices (e.g. all current iPhones) have a Compact vertical size class in landscape mode.

This is a much better concept for general layout decisions than determining the device's orientation.

That being said, if you really need to know the orientation, UIDevice.currentDevice().orientation is the way to go.


Original post:

Override the updateViewConstraints method of UIViewController to provide layout constraints for specific situations. This way, the layout is always set up the correct way according to situation. Make sure they form a complete set of constraints with those created within the storyboard. You can use IB to set up your general constraints and mark those subject to change to be removed at runtime.

I use the following implementation to present a different set of constraints for each orientation:

-(void)updateViewConstraints {
    [super updateViewConstraints];

    // constraints for portrait orientation
    // use a property to change a constraint's constant and/or create constraints programmatically, e.g.:
    if (!self.layoutConstraintsPortrait) {
        UIView *image1 = self.image1;
        UIView *image2 = self.image2;
        self.layoutConstraintsPortrait = [[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[image1]-[image2]-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(image1, image2)] mutableCopy];
        [self.layoutConstraintsPortrait addObject:[NSLayoutConstraint constraintWithItem:image1 attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem: image1.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
        [self.layoutConstraintsPortrait addObject:[NSLayoutConstraint constraintWithItem:image2 attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:image2.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
    }

    // constraints for landscape orientation
    // make sure they don't conflict with and complement the existing constraints
    if (!self.layoutConstraintsLandscape) {
        UIView *image1 = self.image1;
        UIView *image2 = self.image2;
        self.layoutConstraintsLandscape = [[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[image1]-[image2]-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(image1, image2)] mutableCopy];
        [self.layoutConstraintsLandscape addObject:[NSLayoutConstraint constraintWithItem:image1 attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:image1.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
        [self.layoutConstraintsLandscape addObject:[NSLayoutConstraint constraintWithItem:image2 attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem: image2.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
    }

    BOOL isPortrait = UIInterfaceOrientationIsPortrait(self.interfaceOrientation);
    [self.view removeConstraints:isPortrait ? self.layoutConstraintsLandscape : self.layoutConstraintsPortrait];
    [self.view addConstraints:isPortrait ? self.layoutConstraintsPortrait : self.layoutConstraintsLandscape];        
}

Now, all you need to do is trigger a constraint update whenever the situation changes. Override willAnimateRotationToInterfaceOrientation:duration: to animate the constraint update on orientation change:

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];

    [self.view setNeedsUpdateConstraints];

}
Fusionism answered 1/11, 2013 at 1:33 Comment(9)
You don't need manually to "trigger" a constraint update. 'updateViewConstraints' method already does that. This comment was also made by rokridi below, but this answer has not been edited.Wyman
Don't forget that the device might be rotated while your view is backgrounded. You could use setNeedsUpdateConstraints when the app is foregrounded.Murillo
With the release of iOS 8 the concept of Size Classes will solve this problem anyway ;)Fusionism
@knl, size classes don't address portrait vs landscape on iPads (they're both wRegular/hRegular).Accelerometer
On iOS 8 at least, orientation changes (whether while active or in the background) trigger a call to updateViewConstraints without having to do anything.Accelerometer
@Accelerometer Yes, I mentioned this above. Usually though, design choices should not be informed by the device's orientation, but by its size class. If you do need the orientation, you can still use UIDevice.currentDevice().orientation and the approach outlined above.Fusionism
How can i perform same things when I have UIView subclass.? not UIViewController subclass.Tableland
Apple size classes link: developer.apple.com/library/content/documentation/…Haik
@Fusionism can you please add some more info fox xcode 9, that how we can do the same with it?Mirepoix
P
17

The approach I am using (for better or worse) is to define both sets of constraints (portrait and landscape) in the storyboard editor.

To avoid the storyboard warnings of hell, I place all of one set at a priority of 999, just so it doesn't show red everywhere.

Then I add all of the constraints to outlet collections:

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

Finally, I implement my ViewControllers's viewWillLayout method:

- (void) viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    for (NSLayoutConstraint *constraint in self.portraitConstraints) {
        constraint.active = (UIApplication.sharedApplication.statusBarOrientation == UIDeviceOrientationPortrait);
    }
    for (NSLayoutConstraint *constraint in self.landscapeConstraints) {
        constraint.active = (UIApplication.sharedApplication.statusBarOrientation != UIDeviceOrientationPortrait);
    }
}

This seems to work. I really wish you could set the default active property in the the storyboard editor.

Punnet answered 29/12, 2014 at 23:51 Comment(4)
Very Nice, thanks! Unfortunately I have to do this too as I'm supporting iOS 7, so can't make the most of Size classes yet. I have a custom subview and do the same as you do in 'viewWillLayoutSubviews' in an overridden 'updateConstraints', then all I need to do is call setNeedsUpdateConstraints from willAnimateRotationToInterfaceOrientation. It does work fine.Relativity
I'd also like to give you another up mark for setting the priority to avoid warnings.Relativity
NOTE: active property is iOS 8 only. Also, whilst noticing this, I also found some convenience class methods on NSLayoutConstraint.activateConstraints() and NSLayoutConstraint.deactivateConstraints(), in which you can pass in the IBOutletCollection. In iOS 7, it's almost the same technique but I use self.removeConstraint and self.addConstraint, which works in my case but I don't know how feasible this is for all cases.Relativity
I left priority 1000 for all, just unchecked "Installed" in interface builderBergamo
E
2

I am following the same approach as yours (no nib files or storyboards). You have to update your constraints in updateViewConstraints method (by checking the device orientation). There is no need to call setNeedsUpdateConstraints in updateViewConstraints because as soon as you change the device orientation the last method is called automatically.

Echopraxia answered 3/12, 2013 at 10:28 Comment(0)
T
1

For anyone who is searching for current possible solution (Swift), update your constraints in this UIViewController function:

 override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {}

This is called everytime the device is rotated. It allows you to respond to these changes and even animate them. To give you better overview, this is how I've changed my layout in my last project, by changing constraints of two subviews. In portrait mode I had subviews above each other, in landscape mode subviews were side by side.

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    // 1. I recommend to remove existing constraints BEFORE animation, otherwise Xcode can yell at you "Unable to simultaneously satisfy constraints"
    removeConstraintsOfSubview()

    coordinator.animate(alongsideTransition: { [unowned self] context in
        // 2. By comparing the size, you can know whether you should use portrait or landscape layout and according to that remake or add specific constraints
        if size.height > size.width {
            self.setPortraitLayout()
        } else {
            self.setLandscapeLayout()
        }
        // 3. If you want the change to be smoothly animated call this block here
        UIView.animate(withDuration: context.transitionDuration) {
            self.view.layoutIfNeeded()
        }
        }, completion: { _ in
            // After the device is rotated and new constraints are added, you can perform some last touch here (probably some extra animation)
    })
}
Trueblue answered 11/2, 2020 at 19:12 Comment(0)
B
0

You can save your constraints, to a property or variable as portrait and landscape versions then set and remove them on rotate.

I have done this making my constraints in xib for initial view, assigning them to outlets in the view controller. On rotate I create the alternate restraints, remove the outlets but retain them, insert the alternates.

Reverse the process on rotate back.

Behindhand answered 21/7, 2013 at 13:59 Comment(2)
What happens if the device is rotated when the app is off screen? And are there any other edge cases I should be aware of?Murillo
your view will not receive notices of rotation when it is not visible. I am pretty sure of that. In fact I remember seeing that in some threads while searching for some related solutions. I suppose you would need to check the device orientation on viewWillAppear.Behindhand
H
-1

My idea is to handle orientation by changing constraint priorities.
Supposing priorities will be:

  • landscape: 910 active / 10 inactive.
  • portrait: 920 active / 20 inactive.

Step 1: Create landscape (or portrait) design in Storyboard with constraints.
Step 2: For constraints, that must be active only for landscape mode set priority to 10.
Step 3: Add constraints for portrait mode and set their priority to 920.

In willAnimateRotationToInterfaceOrientation add code:

for (NSLayoutConstraint *constraint in myView.constraints) {
    if (UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication.statusBarOrientation)) {
        if (constraint.priority == 10)  constraint.priority = 910;
        if (constraint.priority == 920) constraint.priority = 20;
    } else {
        if (constraint.priority == 20)  constraint.priority = 920;
        if (constraint.priority == 910) constraint.priority = 10;
    }
}

Advantage of this approach - easy adjusting in Interface Builder. When we need to switch to any orientation, we select all constraints by priority and change them simultaneously (910->10, 20->920):

enter image description here

Interface will rebuilded automatically.

Hagerty answered 9/12, 2016 at 17:55 Comment(2)
Man. Constraints' priority appeared to allow to declare constraints once to make app handle them automatically. But you still use them to manually check them on each rotation action.Caulicle
Constraints priority shouldn't be declared once and can be changed on rotation. This approach allows to use less code than any other.Hagerty

© 2022 - 2024 — McMap. All rights reserved.