ChatGPT解决这个技术问题 Extra ChatGPT

How do I animate constraint changes?

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:

https://i.stack.imgur.com/576mp.png

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.

I've never seen so many votes offered for a question and answer on a typo on SO before
If there is a typo in the answer, you should edit the answer. That's why they're editable.
@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.
I don't know what the typo is. I was responding to comments above.
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.

i
iwasrobbed

Two important notes:

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 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()
}

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.
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.
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.
using layoutIfNeeded will animate all the subview refreshes not just the constraint change. how do u animate the constraint change only?
"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.
M
Mikolaj

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.


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 ;)
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.
translatesAutoresizingMaskIntoConstraints = NO; If you want to pure-autolayout you should definitely disable this.
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?
Beware: If using the UIViewAnimationOptionBeginFromCurrentState the layout constraints will be set BEFORE the animation!
S
Steven Hepting

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:

https://i.stack.imgur.com/EuPOs.jpg

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:

https://i.stack.imgur.com/xakEC.gif

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


+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
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.
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)
Great answer. Specially because you mention Martin's and Ken's conversation about this.
I'm confused about the manner of updating constraints and wrote this. Can you please take a look?
J
John Erck
// 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];
}];

M
Milan Nosáľ

Swift 4 solution

UIView.animate

Three simple steps:

Change the constraints, e.g.: heightAnchor.constant = 50 Tell the containing view that its layout is dirty and that the autolayout should recalculate the layout: self.view.setNeedsLayout() 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.


layoutIfNeeded() is the key
K
Karen Hovhannisyan

Swift solution:

yourConstraint.constant = 50
UIView.animate(withDuration: 1.0, animations: {
    yourView.layoutIfNeeded
})

A
Adam Preble

Storyboard, Code, Tips and a few Gotchas

The other answers are just fine but this one highlights a few fairly important gotchas of animating constraints using a recent example. I went through a lot of variations before I realized the following:

Make the constraints you want to target into Class variables to hold a strong reference. In Swift I used lazy variables:

lazy var centerYInflection:NSLayoutConstraint = {
       let temp =  self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
        return temp!
}()

After some experimentation I noted that one MUST obtain the constraint from the view ABOVE (aka the superview) the two views where the constraint is defined. In the example below (both MNGStarRating and UIWebView are the two types of items I am creating a constraint between, and they are subviews within self.view).

Filter Chaining

I take advantage of Swift's filter method to separate the desired constraint that will serve as the inflection point. One could also get much more complicated but filter does a nice job here.

Animating Constraints Using Swift

Nota Bene - This example is the storyboard/code solution and assumes one has made default constraints in the storyboard. One can then animate the changes using code.

Assuming you create a property to filter with accurate criteria and get to a specific inflection point for your animation (of course you could also filter for an array and loop through if you need multiple constraints):

lazy var centerYInflection:NSLayoutConstraint = {
    let temp =  self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
    return temp!
}()

....

Sometime later...

@IBAction func toggleRatingView (sender:AnyObject){

    let aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)

    self.view.layoutIfNeeded()


    //Use any animation you want, I like the bounce in springVelocity...
    UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.75, options: [.CurveEaseOut], animations: { () -> Void in

        //I use the frames to determine if the view is on-screen
        if CGRectContainsRect(self.view.frame, self.ratingView.frame) {

            //in frame ~ animate away
            //I play a sound to give the animation some life

            self.centerYInflection.constant = aPointAboveScene
            self.centerYInflection.priority = UILayoutPriority(950)

        } else {

            //I play a different sound just to keep the user engaged
            //out of frame ~ animate into scene
            self.centerYInflection.constant = 0
            self.centerYInflection.priority = UILayoutPriority(950)
            self.view.setNeedsLayout()
            self.view.layoutIfNeeded()
         }) { (success) -> Void in

            //do something else

        }
    }
}

The many wrong turns

These notes are really a set of tips that I wrote for myself. I did all the don'ts personally and painfully. Hopefully this guide can spare others.

Watch out for zPositioning. Sometimes when nothing is apparently happening, you should hide some of the other views or use the view debugger to locate your animated view. I've even found cases where a User Defined Runtime Attribute was lost in a storyboard's xml and led to the animated view being covered (while working). Always take a minute to read the documentation (new and old), Quick Help, and headers. Apple keeps making a lot of changes to better manage AutoLayout constraints (see stack views). Or at least the AutoLayout Cookbook. Keep in mind that sometimes the best solutions are in the older documentation/videos. Play around with the values in the animation and consider using other animateWithDuration variants. Don't hardcode specific layout values as criteria for determining changes to other constants, instead use values that allow you to determine the location of the view. CGRectContainsRect is one example If needed, don't hesitate to use the layout margins associated with a view participating in the constraint definition let viewMargins = self.webview.layoutMarginsGuide: is on example Don't do work you don't have to do, all views with constraints on the storyboard have constraints attached to the property self.viewName.constraints Change your priorities for any constraints to less than 1000. I set mine to 250 (low) or 750 (high) on the storyboard; (if you try to change a 1000 priority to anything in code then the app will crash because 1000 is required) Consider not immediately trying to use activateConstraints and deactivateConstraints (they have their place but when just learning or if you are using a storyboard using these probably means your doing too much ~ they do have a place though as seen below) Consider not using addConstraints / removeConstraints unless you are really adding a new constraint in code. I found that most times I layout the views in the storyboard with desired constraints (placing the view offscreen), then in code, I animate the constraints previously created in the storyboard to move the view around. I spent a lot of wasted time building up constraints with the new NSAnchorLayout class and subclasses. These work just fine but it took me a while to realize that all the constraints that I needed already existed in the storyboard. If you build constraints in code then most certainly use this method to aggregate your constraints:

Quick Sample Of Solutions to AVOID when using Storyboards

private var _nc:[NSLayoutConstraint] = []
    lazy var newConstraints:[NSLayoutConstraint] = {

        if !(self._nc.isEmpty) {
            return self._nc
        }

        let viewMargins = self.webview.layoutMarginsGuide
        let minimumScreenWidth = min(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height)

        let centerY = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.webview.centerYAnchor)
        centerY.constant = -1000.0
        centerY.priority = (950)
        let centerX =  self.ratingView.centerXAnchor.constraintEqualToAnchor(self.webview.centerXAnchor)
        centerX.priority = (950)

        if let buttonConstraints = self.originalRatingViewConstraints?.filter({

            ($0.firstItem is UIButton || $0.secondItem is UIButton )
        }) {
            self._nc.appendContentsOf(buttonConstraints)

        }

        self._nc.append( centerY)
        self._nc.append( centerX)

        self._nc.append (self.ratingView.leadingAnchor.constraintEqualToAnchor(viewMargins.leadingAnchor, constant: 10.0))
        self._nc.append (self.ratingView.trailingAnchor.constraintEqualToAnchor(viewMargins.trailingAnchor, constant: 10.0))
        self._nc.append (self.ratingView.widthAnchor.constraintEqualToConstant((minimumScreenWidth - 20.0)))
        self._nc.append (self.ratingView.heightAnchor.constraintEqualToConstant(200.0))

        return self._nc
    }()

If you forget one of these tips or the more simple ones such as where to add the layoutIfNeeded, most likely nothing will happen: In which case you may have a half baked solution like this:

NB - Take a moment to read the AutoLayout Section Below and the original guide. There is a way to use these techniques to supplement your Dynamic Animators.

UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 1.0, options: [.CurveEaseOut], animations: { () -> Void in

            //
            if self.starTopInflectionPoint.constant < 0  {
                //-3000
                //offscreen
                self.starTopInflectionPoint.constant = self.navigationController?.navigationBar.bounds.height ?? 0
                self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)

            } else {

                self.starTopInflectionPoint.constant = -3000
                 self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
            }

        }) { (success) -> Void in

            //do something else
        }

    }

Snippet from the AutoLayout Guide (note the second snippet is for using OS X). BTW - This is no longer in the current guide as far as I can see. The preferred techniques continue to evolve.

Animating Changes Made by Auto Layout

If you need full control over animating changes made by Auto Layout, you must make your constraint changes programmatically. The basic concept is the same for both iOS and OS X, but there are a few minor differences.

In an iOS app, your code would look something like the following:

[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
}];

In OS X, use the following code when using layer-backed animations:

[containterView layoutSubtreeIfNeeded];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
     [context setAllowsImplicitAnimation: YES];
     // Make all constraint changes here
     [containerView layoutSubtreeIfNeeded];
}];

When you aren’t using layer-backed animations, you must animate the constant using the constraint’s animator:

[[constraint animator] setConstant:42];

For those who learn better visually check out this early video from Apple.

Pay Close Attention

Often in documentation there are small notes or pieces of code that lead to bigger ideas. For example attaching auto layout constraints to dynamic animators is a big idea.

Good Luck and May the Force be with you.


C
C0mrade

Working Solution 100% Swift 5.3

i have read all the answers and want to share the code and hierarchy of lines which i have used in all my applications to animate them correctly, Some solutions here are not working, you should check them on slower devices e.g iPhone 5 at this moment.

self.btnHeightConstraint.constant = 110
UIView.animate(withDuration: 0.27) { [weak self] in 
     self?.view.layoutIfNeeded()
}

G
Gabriel.Massana

I was trying to animate Constraints and was not really easy to found a good explanation.

What other answers are saying is totally true: you need to call [self.view layoutIfNeeded]; inside animateWithDuration: animations:. However, the other important point is to have pointers for every NSLayoutConstraint you want to animate.

I created an example in GitHub.


E
Edoardo Vicoli

Working and just tested solution for Swift 3 with Xcode 8.3.3:

self.view.layoutIfNeeded()
self.calendarViewHeight.constant = 56.0

UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
        self.view.layoutIfNeeded()
    }, completion: nil)

Just keep in mind that self.calendarViewHeight is a constraint referred to a customView (CalendarView). I called the .layoutIfNeeded() on self.view and NOT on self.calendarView

Hope this help.


D
D.D.

There is an article talk about this: http://weblog.invasivecode.com/post/42362079291/auto-layout-and-core-animation-auto-layout-was

In which, he coded like this:

- (void)handleTapFrom:(UIGestureRecognizer *)gesture {
    if (_isVisible) {
        _isVisible = NO;
        self.topConstraint.constant = -44.;    // 1
        [self.navbar setNeedsUpdateConstraints];  // 2
        [UIView animateWithDuration:.3 animations:^{
            [self.navbar layoutIfNeeded]; // 3
        }];
    } else {
        _isVisible = YES;
        self.topConstraint.constant = 0.;
        [self.navbar setNeedsUpdateConstraints];
        [UIView animateWithDuration:.3 animations:^{
            [self.navbar layoutIfNeeded];
        }];
    }
}

Hope it helps.


i
ivanferdelja

In the context of constraint animation, I would like to mention a specific situation where I animated a constraint immediately within a keyboard_opened notification.

Constraint defined a top space from a textfield to top of the container. Upon keyboard opening, I just divide the constant by 2.

I was unable to achieve a conistent smooth constraint animation directly within the keyboard notification. About half the times view would just jump to its new position - without animating.

It occured to me there might be some additional layouting happening as result of keyboard opening. Adding a simple dispatch_after block with a 10ms delay made the animation run every time - no jumping.