Restoring NSLayoutConstraints after a change

author: Kamil Burczyk

If you ever had a situation when your precisely crafted Auto Layout view had to be transformed (moved, resized etc.) and then changed back to its original constraints, you should definitely read this post and presented solution :)

My case started during development of library that needed to place a view in a container defined by the programmer of app that uses this lib and be able to resize itself and this container so it could display all its content.

When you position UIView either in Interface Builder or by setting NSLayoutConstraints by yourself you can always connect them to properties and change only constant value.

  1. Connect your constraint by pressing control and mouse left button on this particular constraint and dragging blue line to your UIViewController's text file.

  2. Change the .constant property of created NSLayoutConstraint whenever you want to resize this view.

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *constraintHeight;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    _constraintHeight.constant = 200;
}

@end

Quick and easy. The only thing is when you want to come back to view's original size you have to remember it somewhere and then reset .constant to its original value.

Situation becomes more complicated if you have multiple constraints - you have to remember initial values of all of them.

You can have a great idea (like I did):

why not to copy all of the constraints, set my own and then restore saved copy? NSArray has even a special method for that case which performs deep copy of every object: - (instancetype)initWithArray:(NSArray *)array copyItems:(BOOL)flag;.

Just for the record [NSarray arrayWithArray:] will not work because it creates only a new pointer to original array and when we will change original objects they will change in our copy, because in fact these are the same pointers.

So you type something like NSArray *constraintsCopy = [NSArray initWithArray:self.view.constraints]; and you get runtime error: -[NSLayoutConstraint copyWithZone:]: unrecognized selector sent to instance 0x8a5acb0

One look into NSLayoutConstraint.h file and you see that @interface NSLayoutConstraint : NSObject { does not conform to <NSCopying> protocol, so constraints are not copyable. This path is probably closed.

Another great idea:

we can have references to all constraints, detach them for a while, set our own constraints and when view can come back to its original size and position we just remove ours constraints and re-add referenced constraints.
Let's see how constraints of our view look like.

Basic constraints

Unfortunately it has only 2 constraints assigned to itself, 2 other are assigned to its parent which can be seen in the left panel of IB. If we print those constraints

NSLog(@"constraints: %@", _redSquare.constraints);  
NSLog(@"superview constraints: %@", _redSquare.superview.constraints);  

we will get something like:

2014-02-01 17:18:14.213 AutolayoutExample[70899:70b] constraints: (  
    "<NSLayoutConstraint:0x8d94430 H:[UIView:0x8d94fd0(100)]>",
    "<NSLayoutConstraint:0x8d94fa0 V:[UIView:0x8d94fd0(200)]>"
)
2014-02-01 17:18:14.215 AutolayoutExample[70899:70b] superview constraints: (  
    "<NSLayoutConstraint:0x8d95d80 H:|-(20)-[UIView:0x8d94fd0]   (Names: '|':UIView:0x8d94560 )>",
    "<NSLayoutConstraint:0x8d95db0 V:[_UILayoutGuide:0x8d953f0]-(0)-[UIView:0x8d94fd0]>",
    "<_UILayoutSupportConstraint:0x8d93a70 V:[_UILayoutGuide:0x8d953f0(0)]>",
    "<_UILayoutSupportConstraint:0x8d95de0 V:|-(0)-[_UILayoutGuide:0x8d953f0]   (Names: '|':UIView:0x8d94560 )>",
    "<_UILayoutSupportConstraint:0x8d94750 V:[_UILayoutGuide:0x8d95960(0)]>",
    "<_UILayoutSupportConstraint:0x8d95810 _UILayoutGuide:0x8d95960.bottom == UIView:0x8d94560.bottom>"
)

Logs confirm that our view only possesses constraints for its width and height, but its margins are configured by its superview.
So if we did something like:

[_redSquare removeConstraints:_redSquare.constraints];

it would remove only width and height constraints but it would leave margins untouched.
In act of despair you could try to call:

[_redSquare.superview removeConstraints:_redSquare.constraints];

(or even better call it recursively on each superview between your view and UIWindow) but let's agreee it's not a very clever idea ;)

My solution

Define whole interface with constraints of lower priority e.g. UILayoutPriorityDefaultHigh (which has a numerical value of 750 in moment of writing this article).
If you want to resize or reposition view you just add new constraints with higher priority like UILayoutPriorityRequired (1000) and AutoLayout engine makes all the hard work.

You can see it as having 2 separate but complete layouts written in constraints, one with lower priority, other with higher. During calculating layout the 'stronger' one will just magically overload the 'weaker'.

Constraints with lower priority

With this approach you can completly abandon old constraints, you just have to take care of yours.

Complete implementation of resize and reset IBActions with fancy animations can look like this:

- (IBAction)resizeView:(UIButton *)sender {
    NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(100)-[_redSquare(200)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_redSquare)];
    NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[_redSquare(200)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_redSquare)];

    _constraintsWithHigherPriority = [_constraintsWithHigherPriority arrayByAddingObjectsFromArray:verticalConstraints];
    _constraintsWithHigherPriority = [_constraintsWithHigherPriority arrayByAddingObjectsFromArray:horizontalConstraints];

    [_redSquare.superview addConstraints:_constraintsWithHigherPriority];

    [UIView animateWithDuration:1.0f animations:^{
        [self.view layoutIfNeeded];
    }];
}

- (IBAction)moveBackToOriginalPosition:(UIButton *)sender {
    [_redSquare.superview removeConstraints:_constraintsWithHigherPriority];

    [UIView animateWithDuration:1.0f animations:^{
        [self.view layoutIfNeeded];
    }];
}

With proposed approach you will not get ambiguous layout because of different priorities. In fact you could define ~1000 layouts (assuming that .priority value can be initialized with something from (0-1000) range.

Constraints after resize:

2014-02-01 19:34:56.778 AutolayoutExample[71744:70b] constraints after resize: (  
    "<NSLayoutConstraint:0x8b3c420 H:[UIView:0x8b3c160(100@750)] priority:750>",
    "<NSLayoutConstraint:0x8b3c110 V:[UIView:0x8b3c160(100@750)] priority:750>"
)

2014-02-01 19:34:56.780 AutolayoutExample[71744:70b] superview constraints after resize: (  
    "<NSLayoutConstraint:0x8b3d070 H:|-(20@750)-[UIView:0x8b3c160] priority:750   (Names: '|':UIView:0x8b309f0 )>",
    "<NSLayoutConstraint:0x8b3d0a0 V:[_UILayoutGuide:0x8b3c660]-(0@750)-[UIView:0x8b3c160] priority:750>",


    "<NSLayoutConstraint:0x8d2e6c0 V:|-(100)-[UIView:0x8b3c160]   (Names: '|':UIView:0x8b309f0 )>",
    "<NSLayoutConstraint:0x8d64ba0 V:[UIView:0x8b3c160(200)]>",
    "<NSLayoutConstraint:0x8d2ff90 H:|-(50)-[UIView:0x8b3c160]   (Names: '|':UIView:0x8b309f0 )>",
    "<NSLayoutConstraint:0x8d2eef0 H:[UIView:0x8b3c160(200)]>"
)

Four last are the one we added.

All you have to do when you want to move back to previous layout is to remove all your constraints:

[_redSquare.superview removeConstraints:_constraintsWithHigherPriority];

and you're done.

Pros of solution with multiple layouts with different priorities:

  • you don't have to deal with someone elses constraints
  • you override old layout with stronger one, just like in OOP
  • setting costraints remains declarative, without changing single properties of single constraints

You can find complete project used as example on github.

If you have any thoughts or better solution just leave a comment or send me an email at kamil.burczyk@sigmapoint.pl

comments powered by Disqus