Are NSLayoutConstraints animatable?

IosAnimationAutolayout

Ios Problem Overview


I am trying to animate up some views so that they are blocked by the giant keyboard in landscape. It works well if I simply animate the frames, but others have suggested that this is counter-productive and I should be updating the NSLayoutConstraints instead. However, they don't seem to be animatable. Has anyone gotten them to work with success?

//heightFromTop is an NSLayoutConstraint referenced from IB
[UIView animateWithDuration:0.25 animations:^{    self.heightFromTop.constant= 550.f;}];

The result is an instant jump to the height in question.

Ios Solutions


Solution 1 - Ios

Just follow this exact pattern:

self.heightFromTop.constant = 550.0f;
[myView setNeedsUpdateConstraints];

[UIView animateWithDuration:0.25f animations:^{
   [myView layoutIfNeeded];
}];

where myView is the view where self.heightFromTop was added to. Your view is "jumping" because the only thing you did in the animation block was to set the constraint, which does not cause layouts immediately. In your code, the layout happens on the next run loop after you set heightFromTop.constant, and by that time you are already outside the scope of the animation block.

In Swift 2:

self.heightFromTop.constant = 550
myView.setNeedsUpdateConstraints()

UIView.animateWithDuration(0.25, animations: {
   myView.layoutIfNeeded()
})

Solution 2 - Ios

The Apple's suggested way is a little bit different (See example in "Animating Changes Made by Auto Layout" section). First you need to call layoutIfNeeded before the animation. Then add your animation stuff inside the animation block and then call layoutIfNeeded at the end again. For the guys like me who are transitioning to autolayout, its more similar way to previous animations that we were doing with the frames inside animation blocks. We just need to call layoutIfNeeded twice - before animations and after animations:

[self.view layoutIfNeeded]; // Ensures that all pending layout operations have been completed

[UIView animateWithDuration:1.0f animations:^{
  
  // Make all constraint changes here
  self.heightFromTop.constant= 550.f;

  [self.view layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes

}];

Solution 3 - Ios

I tried @Centurion's approach, but somehow my view would animate to a wrong frame if it's loaded from the storyboard. The issue goes away if I replace the first layoutIfNeeded with updateConstraintsIfNeeded, though I have no idea why. If anyone can give an explanation it'd be much appreciated.

[self.view updateConstraintsIfNeeded];
[UIView animateWithDuration:1.0 animations:^{
    self.myConstraint.constant= 100;
    [self.view layoutIfNeeded];
}];

Solution 4 - Ios

I was having a similar problem and this thread was a great help to get past it.

The answer from erurainon put me on the right track, but I'd like to propose a slightly different answer. The suggested code from erurainon did not work for me, as I still had a jump instead of an animated transition. The link provided by cnotethegr8 gave me the working answer:

Auto Layout Guide https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/AutoLayoutbyExample/AutoLayoutbyExample.html (all the way to the bottom of the page).

A few differences from the answer by erurainon:

  1. Call layoutIfNeeded on the container view before the call to an animation method (and instead of setNeedsUpdateConstraints on myView).
  2. Set the new constraint in the animations block.
  3. Call layoutIfNeeded on the container view in the animations method (after setting the constraint), instead of on myView.

This will adhere to the pattern suggested by Apple in the link above.

An example

I wanted to animate a particular view, closing or expanding it at the click of a button. Since I'm using autolayout and didn't want to hard code any dimensions (in my case height) in the code, I decided to capture the height in viewDidLayoutSubviews. You need to use this method and not viewWillAppear when using autolayout. Since viewDidLayoutSubviews may be called many times, I used a BOOL to let me know about the first execution for my initialization.

// Code snippets

@property (weak, nonatomic) IBOutlet UIView *topView; // Container for minimalView
@property (weak, nonatomic) IBOutlet UIView *minimalView; // View to animate

@property (nonatomic) CGFloat minimalViewFullHeight; // Original height of minimalView

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

@property (nonatomic) BOOL executedViewDidLayoutSubviews;


- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];


    // First execution of viewDidLayoutSubviews?
    if(!self.executedViewDidLayoutSubviews){
        self.executedViewDidLayoutSubviews = YES;
    
    
        // Record some original dimensions
        self.minimalViewFullHeight = self.minimalView.bounds.size.height;
    
    
        // Setup our initial view configuration & let system know that 
        // constraints need to be updated.
        self.minimalViewHeightConstraint.constant = 0.0;
        [self.minimalView setNeedsUpdateConstraints];
    
        [self.topView layoutIfNeeded];
    }
}

Resize full action snippet

// An action to close our minimal view and show our normal (full) view
- (IBAction)resizeFullAction:(UIButton *)sender {

    [self.topView layoutIfNeeded];

    [UIView transitionWithView:self.minimalView
                  duration:1.0
                   options:UIViewAnimationOptionTransitionCrossDissolve
                animations:^{
                    self.minimalViewHeightConstraint.constant = 0.0;

                    // Following call to setNeedsUpdateConstraints may not be necessary
                    [self.minimalView setNeedsUpdateConstraints];
                    
                    [self.topView layoutIfNeeded];
                    
                } completion:^(BOOL finished) {
                    ;
                }];

    // Other code to show full view
    // ...
}

Resize small action snippet

// An action to open our minimal view and hide our normal (full) view
- (IBAction)resizeSmallAction:(UIButton *)sender {

    [self.topView layoutIfNeeded];

    [UIView transitionWithView:self.minimalView
                  duration:1.0
                   options:UIViewAnimationOptionTransitionCrossDissolve
                animations:^{
                    self.minimalViewHeightConstraint.constant = self.minimalViewFullHeight;
                    [self.minimalView setNeedsUpdateConstraints];
                    
                    [self.topView layoutIfNeeded];
                    
                } completion:^(BOOL finished) {
                    ;
                }];

        // Other code to hide full view
        // ...
    }

You can use animateWithDuration instead of transitionWithView if you wish.

Hope this helps.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionborrrdenView Question on Stackoverflow
Solution 1 - IosJohn EstropiaView Answer on Stackoverflow
Solution 2 - IosCenturionView Answer on Stackoverflow
Solution 3 - IosJoseph LinView Answer on Stackoverflow
Solution 4 - IosScott CarterView Answer on Stackoverflow