Episode #135

Animating Constraints

15 minutes
Published on September 4, 2014

This video is only available to subscribers. Get access to this video and 586 others.

What good is a static layout? When specifying layout using constraints, we still need to provide transitions and other animations in our interfaces. We can do this quite easily by just animating between different sets of constraints.

Episode Links

Animating the Left View

We need some properties to be able to re-generate constraints when the animation occurs:

@property (nonatomic, strong) NSLayoutConstraint *leftWidthConstraint;
@property (nonatomic, strong) UIView *leftView;
@property (nonatomic, strong) UIView *headlineView;
@property (nonatomic) BOOL expanded;

Then we extract a method that can define our left view constraints given a passed in width:

- (NSLayoutConstraint *)leftWidthConstraintWithMultiplier:(CGFloat)multiplier {
    return [NSLayoutConstraint constraintWithItem:self.leftView
                                        attribute:NSLayoutAttributeWidth
                                        relatedBy:NSLayoutRelationEqual
                                           toItem:self.headlineView
                                        attribute:NSLayoutAttributeWidth
                                       multiplier:multiplier
                                         constant:0];
}

And then set up the initial state in viewDidLoad:

    self.leftWidthConstraint = [self leftWidthConstraintWithMultiplier:0.3];
    [self.view addConstraint:self.leftWidthConstraint];

Then, when the button is tapped:

- (void)onButtonTap:(id)sender {
    CGFloat multiplier = self.expanded ? 0.3 : 0.7;
    self.expanded = !self.expanded;
    [self.view removeConstraint:self.leftWidthConstraint];
    self.leftWidthConstraint = [self leftWidthConstraintWithMultiplier:multiplier];
    [self.view addConstraint:self.leftWidthConstraint];
    [UIView animateWithDuration:0.5
                          delay:0
         usingSpringWithDamping:0.7
          initialSpringVelocity:4
                        options:0
                     animations:^{
                         [self.view layoutIfNeeded];
                     } completion:nil];
}

Note that we simply animate the layoutIfNeeded method call. Changing the constraints doesn't immediately change any layout, allowing you to be temporarily in an inconsistent state while you build up the necessary constraints to fully specify the layout. Then when you are ready, you call layoutIfNeeded.

Animating in the Accessory View

We're going to do a similar thing for the accessory view. Starting with some extra properties we'll need:

@property (nonatomic, strong) UIView *accessoryView;
@property (nonatomic, strong) NSArray *verticalConstraints;
@property (nonatomic) BOOL accessoryVisible

Then we create a function that will return our vertical constraints:

- (NSArray *)verticalConstraintsWithAccessoryViewVisible:(BOOL)visible {
    NSDictionary *metrics = @{
                              @"topPadding": visible ? @40 : @60,
                              @"headlineViewHeight": visible ? @80 : @100,
                              @"accessoryPadding": visible ? @8 : @0,
                              @"accessoryHeight" : visible ? @100 : @0
                              };
    return [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(topPadding)-[_headlineView(headlineViewHeight)]-[_leftView]-(accessoryPadding)-[_accessoryView(accessoryHeight)]-|"
                                             options:NSLayoutFormatAlignAllLeft
                                             metrics:metrics
                                                     views:NSDictionaryOfVariableBindings(_headlineView, _leftView, _accessoryView)];
}

Notice that we have a zero height padding and view if the view is invisible.

We can set the initial state in viewDidLoad:

    self.verticalConstraints = [self verticalConstraintsWithAccessoryViewVisible:NO];
    [self.view addConstraints:self.verticalConstraints];

Then when we tap on the left view:

- (void)onLeftViewTap:(id)sender {
    self.accessoryVisible = !self.accessoryVisible;
    [self.view removeConstraints:self.verticalConstraints];
    self.verticalConstraints = [self verticalConstraintsWithAccessoryViewVisible:self.accessoryVisible];
    [self.view addConstraints:self.verticalConstraints];
    [UIView animateWithDuration:0.5
                          delay:0
         usingSpringWithDamping:0.7
          initialSpringVelocity:4
                        options:0
                     animations:^{
                         [self.view layoutIfNeeded];
                     } completion:nil];
}