How do I animate constraint changes?

320,626

Solution 1

Two important notes:

  1. You need to call layoutIfNeeded within the animation block. Apple actually recommends you call it once before the animation block to ensure that all pending layout operations have been completed

  2. You need to call it specifically on the parent view (e.g. self.view), not the child view that has the constraints attached to it. Doing so will update all constrained views, including animating other views that might be constrained to the view that you changed the constraint of (e.g. View B is attached to the bottom of View A and you just changed View A's top offset and you want View B to animate with it)

Try this:

Objective-C

- (void)moveBannerOffScreen {
    [self.view layoutIfNeeded];

    [UIView animateWithDuration:5
        animations:^{
            self._addBannerDistanceFromBottomConstraint.constant = -32;
            [self.view layoutIfNeeded]; // Called on parent view
        }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen { 
    [self.view layoutIfNeeded];

    [UIView animateWithDuration:5
        animations:^{
            self._addBannerDistanceFromBottomConstraint.constant = 0;
            [self.view layoutIfNeeded]; // Called on parent view
        }];
    bannerIsVisible = TRUE;
}

Swift 3

UIView.animate(withDuration: 5) {
    self._addBannerDistanceFromBottomConstraint.constant = 0
    self.view.layoutIfNeeded()
}

Solution 2

I appreciate the answer provided, but I think it would be nice to take it a bit further.

The basic block animation from the documentation

[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
     // Make all constraint changes here
     [containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];

but really this is a very simplistic scenario. What if I want to animate subview constraints via the updateConstraints method?

An animation block that calls the subviews updateConstraints method

[self.view layoutIfNeeded];
[self.subView setNeedsUpdateConstraints];
[self.subView updateConstraintsIfNeeded];
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionLayoutSubviews animations:^{
    [self.view layoutIfNeeded];
} completion:nil];

The updateConstraints method is overridden in the UIView subclass and must call super at the end of the method.

- (void)updateConstraints
{
    // Update some constraints

    [super updateConstraints];
}

The AutoLayout Guide leaves much to be desired but it is worth reading. I myself am using this as part of a UISwitch that toggles a subview with a pair of UITextFields with a simple and subtle collapse animation (0.2 seconds long). The constraints for the subview are being handled in the UIView subclasses updateConstraints methods as described above.

Solution 3

Generally, you just need to update constraints and call layoutIfNeeded inside the animation block. This can be either changing the .constant property of an NSLayoutConstraint, adding remove constraints (iOS 7), or changing the .active property of constraints (iOS 8 & 9).

Sample Code:

[UIView animateWithDuration:0.3 animations:^{
    // Move to right
    self.leadingConstraint.active = false;
    self.trailingConstraint.active = true;

    // Move to bottom
    self.topConstraint.active = false;
    self.bottomConstraint.active = true;

    // Make the animation happen
    [self.view setNeedsLayout];
    [self.view layoutIfNeeded];
}];

Sample Setup:

Xcode Project so sample animation project.

Controversy

There are some questions about whether the constraint should be changed before the animation block, or inside it (see previous answers).

The following is a Twitter conversation between Martin Pilkington who teaches iOS, and Ken Ferry who wrote Auto Layout. Ken explains that though changing constants outside of the animation block may currently work, it's not safe and they should really be change inside the animation block. https://twitter.com/kongtomorrow/status/440627401018466305

Animation:

Sample Project

Here's a simple project showing how a view can be animated. It's using Objective C and animates the view by changing the .active property of several constraints. https://github.com/shepting/SampleAutoLayoutAnimation

Solution 4

// Step 1, update your constraint
self.myOutletToConstraint.constant = 50; // New height (for example)

// Step 2, trigger animation
[UIView animateWithDuration:2.0 animations:^{

    // Step 3, call layoutIfNeeded on your animated view's parent
    [self.view layoutIfNeeded];
}];

Solution 5

Swift 4 solution

UIView.animate

Three simple steps:

  1. Change the constraints, e.g.:

    heightAnchor.constant = 50
    
  2. Tell the containing view that its layout is dirty and that the autolayout should recalculate the layout:

    self.view.setNeedsLayout()
    
  3. In animation block tell the layout to recalculate the layout, which is equivalent of setting the frames directly (in this case the autolayout will set the frames):

    UIView.animate(withDuration: 0.5) {
        self.view.layoutIfNeeded()
    }
    

Complete simplest example:

heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
    self.view.layoutIfNeeded()
}

Sidenote

There is an optional 0th step - before changing the constraints you might want to call self.view.layoutIfNeeded() to make sure that the starting point for the animation is from the state with old constraints applied (in case there were some other constraints changes that should not be included in animation):

otherConstraint.constant = 30
// this will make sure that otherConstraint won't be animated but will take effect immediately
self.view.layoutIfNeeded()

heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
    self.view.layoutIfNeeded()
}

UIViewPropertyAnimator

Since with iOS 10 we got a new animating mechanism - UIViewPropertyAnimator, we should know that basically the same mechanism applies to it. The steps are basically the same:

heightAnchor.constant = 50
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
    self.view.layoutIfNeeded()
}
animator.startAnimation()

Since animator is an encapsulation of the animation, we can keep reference to it and call it later. However, since in the animation block we just tell the autolayout to recalculate the frames, we have to change the constraints before calling startAnimation. Therefore something like this is possible:

// prepare the animator first and keep a reference to it
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
    self.view.layoutIfNeeded()
}

// at some other point in time we change the constraints and call the animator
heightAnchor.constant = 50
self.view.setNeedsLayout()
animator.startAnimation()

The order of changing constraints and starting an animator is important - if we just change the constraints and leave our animator for some later point, the next redraw cycle can invoke autolayout recalculation and the change will not be animated.

Also, remember that a single animator is non-reusable - once you run it, you cannot "rerun" it. So I guess there is not really a good reason to keep the animator around, unless we use it for controlling an interactive animation.

Share:
320,626
DBD
Author by

DBD

I code therefore I am.

Updated on April 27, 2021

Comments

  • DBD
    DBD about 3 years

    I'm updating an old app with an AdBannerView and when there is no ad, it slides off screen. When there is an ad it slides on the screen. Basic stuff.

    Old style, I set the frame in an animation block. New style, I have a IBOutlet to the auto-layout constraint which determines the Y position, in this case it's distance from the bottom of the superview, and modify the constant:

    - (void)moveBannerOffScreen {
        [UIView animateWithDuration:5 animations:^{
            _addBannerDistanceFromBottomConstraint.constant = -32;
        }];
        bannerIsVisible = FALSE;
    }
    
    - (void)moveBannerOnScreen {
        [UIView animateWithDuration:5 animations:^{
            _addBannerDistanceFromBottomConstraint.constant = 0;
        }];
        bannerIsVisible = TRUE;
    }
    

    And the banner moves, exactly as expected, but no animation.


    UPDATE: I re-watched WWDC 12 talk Best Practices for Mastering Auto Layout which covers animation. It discusses how to update constraints using CoreAnimation:

    I've tried with the following code, but get the exact same results:

    - (void)moveBannerOffScreen {
        _addBannerDistanceFromBottomConstraint.constant = -32;
        [UIView animateWithDuration:2 animations:^{
            [self.view setNeedsLayout];
        }];
        bannerIsVisible = FALSE;
    }
    
    - (void)moveBannerOnScreen {
        _addBannerDistanceFromBottomConstraint.constant = 0;
        [UIView animateWithDuration:2 animations:^{
            [self.view setNeedsLayout];
        }];
        bannerIsVisible = TRUE;
    }
    

    On a side note, I have checked numerous times and this is being executed on the main thread.

    • abbood
      abbood over 10 years
      I've never seen so many votes offered for a question and answer on a typo on SO before
    • jamil ahmed
      jamil ahmed over 9 years
      If there is a typo in the answer, you should edit the answer. That's why they're editable.
    • DBD
      DBD over 9 years
      @jeffamaphone - It would be more useful if you pointed out the typo so I knew where the mistake was. You could edit the answer yourself and fixed the typo saving everyone else our diatribe. I did just edit it to remove the constant from the animation block, if that's what you were referring to.
    • jamil ahmed
      jamil ahmed over 9 years
      I don't know what the typo is. I was responding to comments above.
    • DBD
      DBD over 9 years
      Then the typo is the question. Stupidly I was typing "setNeedsLayout" instead of "layoutIfNeeded". It's shown clearly in my question when I cut and paste my code with the error and the screenshots with the correct command. Yet couldn't seem to notice it til someone pointed it out.
  • g3rv4
    g3rv4 over 11 years
    You have tried doing the change in the constant and layoutIfNeeded both inside the animation block, right?
  • DBD
    DBD over 11 years
    yes, I've tried that. I had it that way originally and only modified after a second watch on the WWDC video.
  • DBD
    DBD over 11 years
    There are no other constraint changes. The constraints on the ad banner are distance from left edge(0), distance from right edge(0), fixed height(32) and distance from bottom(0). There are no other constraints associated with the banner. Commenting out the constraint change will means nothing occurs.
  • DBD
    DBD over 11 years
    You know what... your answer works. The WWDC works.... my vision fails. For some reason it took me a week to realize I was calling setNeedsLayout instead of layoutIfNeeded. I'm slightly horrified by how many hours I spent not noticing I just just typed the wrong method name.
  • Ortwin Gentz
    Ortwin Gentz over 11 years
    The solution works but you don't need to change the constraint constant within the animation block. It's totally fine to set the constraint once before kicking off the animation. You should edit your answer.
  • Oliver Pearmain
    Oliver Pearmain about 11 years
    This didn't work for me initially and then I realized you need to call layoutIfNeeded on the PARENT view, not the view the constraints apply to.
  • ngb
    ngb over 10 years
    using layoutIfNeeded will animate all the subview refreshes not just the constraint change. how do u animate the constraint change only?
  • anneblue
    anneblue about 10 years
    When calling all the methods above on self.view (and not on a subview of that view), calling updateConstraintsIfNeeded is not necessary (because setNeedsLayout also triggers updateConstraints of that view). Might be trivial for the most, but it was not for me until now ;)
  • Cameron Lowell Palmer
    Cameron Lowell Palmer about 10 years
    It is hard to comment without knowing the full interplay of Autolayout and springs and struts layout in your particular case. I'm speaking entirely about a pure autolayout scenario. I would be curious to know what exactly is happening in your layoutSubviews method.
  • Cameron Lowell Palmer
    Cameron Lowell Palmer about 10 years
    translatesAutoresizingMaskIntoConstraints = NO; If you want to pure-autolayout you should definitely disable this.
  • Cameron Lowell Palmer
    Cameron Lowell Palmer about 10 years
    I haven't seen that, but I can't really speak to the issue without some code. Maybe you could mock up a TableView and stick it on GitHub?
  • Robert
    Robert over 9 years
    Beware: If using the UIViewAnimationOptionBeginFromCurrentState the layout constraints will be set BEFORE the animation!
  • natbro
    natbro over 9 years
    works great - thanks. but just so we're clear: it is pure insanity for constraints to basically exist in an entirely different scope for animation. this really guts some very clean older animation logic.
  • Jesse
    Jesse about 9 years
    I couldnt get this to working, thinking i had to call layoutIfNeeded on the parent view of the constraint. I changed it to the parent view of the view controller and now its works beautifully
  • Victor
    Victor almost 9 years
    Also be careful with UILabels. Some type animation with UILabel works differently against the same code with simple UIView . Unfortunately, I don't know why. And if someone knows about it I really appreciate to comment me.
  • user3344977
    user3344977 almost 9 years
    This works perfectly and I'd like to note I'm calling layoutIfNeeded on the subview, not it's parent view. Not really sure why are others are having to call it on the parent.
  • Rick van der Linde
    Rick van der Linde over 8 years
    "Apple actually recommends you call it once before the animation block to ensure that all pending layout operations have been completed", thank you, never thought about that, but it makes sense.
  • Korey Hinton
    Korey Hinton over 8 years
    +1 for showing an example using the new active flag. Also, changing the constraint outside the animation block always felt like a hack to me
  • malhal
    malhal over 8 years
    I raised an issue in github because this solution doesn't work for moving the view back to where it was. I believe changing active isn't the correct solution and you should be changing the priority instead. Set the top to 750, and the bottom to 250, then in code alternate between UILayoutPriorityDefaultHigh and UILayoutPriorityDefaultLow.
  • James Toomey
    James Toomey almost 8 years
    I liked this but wanted the EaseIn effect, and found this works nicely in Swift: UIView.animateWithDuration(0.3, delay: 0, options: UIViewAnimationOptions.CurveEaseIn, animations: { self.view.layoutIfNeeded() }, completion: nil)
  • Chris Conover
    Chris Conover almost 8 years
    Another reason for updating inside the block is that it isolates the changes from calling code that may happen to also call layoutIfNeeded. (and thanks for the Twitter link)
  • Jonny
    Jonny over 7 years
    This does not work with related constraints. Let's say we animate a height A with this, height B that is dependent on height A will be shorter when height A is long, and longer when height A is short. B will unfortunately change right away and will NOT animate.
  • He Yifei 何一非
    He Yifei 何一非 over 7 years
    Sometimes the animation don't animate and run straight to completion. How to solve it?
  • Rool Paap
    Rool Paap over 7 years
    I've implemented your solution multiple times successfully, today it wouldn't work for a new implementation. Found out you need to call self.view.layoutIfNeeded() in the animation block, NOT the completion block. Doh!
  • Rob
    Rob over 7 years
    The "Apple actually recommends" link is broken. If someone knows the new URL can they please update?
  • art-divin
    art-divin over 7 years
    Code example would look better if in the callback one would call [subview.superview layoutIfNeeded];
  • Viktor Kucera
    Viktor Kucera almost 7 years
    @ngb Setup all constraint you don't want to animate -> call layoutIfNeeded and then setup animation block with that one constraint you want to animate, another layoutIfNeeded included.
  • Chauyan
    Chauyan over 6 years
    Yes, it works on iOS 11 beta5, and dont need to update the constraint inside animation block.
  • Eugenio
    Eugenio over 6 years
    Initially it didn't worked for me but then I realised that I've to put isHidden = false out of the animation block and inside the completion block instead
  • Jona
    Jona over 6 years
    Great answer. Specially because you mention Martin's and Ken's conversation about this.
  • mfaani
    mfaani over 6 years
    I'm confused about the manner of updating constraints and wrote this. Can you please take a look?
  • mfaani
    mfaani over 6 years
    I'm confused about the manner of updating constraints and wrote this. Can you please take a look?
  • mfaani
    mfaani over 6 years
    I'm confused about the manner of updating constraints and wrote this. Can you please take a look?
  • Medhi
    Medhi about 4 years
    layoutIfNeeded() is the key
  • Paul T.
    Paul T. over 3 years
    that's not correct. Constraints should be updated outside of the animation block and inside the animation block you should have only view.layoutIfNeeded()
  • Paul T.
    Paul T. over 3 years
    that's not correct. Constraints should be updated outside of the animation block and inside the animation block you should have only view.layoutIfNeeded()