206

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.

borrrden
  • 33,256
  • 8
  • 74
  • 109
  • Since you've tried it and the result was no, chances are it's not (directly) animatable. If someone tells you to use the constraints method, press them directly on the animation issue. – Kevin Oct 17 '12 at 03:22
  • Heres Apples documentation on this. (All the way at the bottom.) https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/AutoLayoutbyExample/AutoLayoutbyExample.html#//apple_ref/doc/uid/TP40010853-CH5-SW1 – cnotethegr8 Oct 17 '13 at 09:29
  • Using layout constraints is the way to do this now. Here's a video tutorial on how you can do this mostly in storyboard instead of manually typing up and maintaining layout constraints. https://www.youtube.com/watch?v=8KVKXlh6sKI – Arjay Waran Nov 15 '15 at 21:31

4 Answers4

491

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()
})
Luke
  • 7,110
  • 6
  • 45
  • 74
John Estropia
  • 17,460
  • 4
  • 46
  • 50
  • 1
    Oh, I see what I was doing wrong now (I was using this pattern before but still with no success)...I was calling `setNeedsLayout` instead of `layoutIfNeeded` Surely a DOH moment! Actually just changing a constraint automatically calls `setNeedsLayout` so I guess `layoutIfNeeded` overrides it somehow. – borrrden Oct 17 '12 at 03:28
  • 12
    You are correct that updating a constraint automatically invokes `setNeedsLayout`; but the purpose of `setNeedsUpdateConstraints` is that it tells the view tree to recompute the constraints; we may have other constraints that are dependent on the one we just changed. In your case it may be OK without that, but if there's another view that for example sticks to the view's bottom, then that view will not know that it needs to update. The pattern above is a safe pattern and I believe should be good practice to observe. – John Estropia Oct 17 '12 at 03:42
  • 1
    Just a small addition. I used it in `viewDidLoad` which caused undesired behavior. I needed to call it in `viewDidAppear`. – Xazen May 24 '13 at 07:11
  • @erurainon, I have the same issue - immediate change instead of animation even if `[view layoutIfNeeded]` is called in animation block. Do you have any glue why it can happen? – Valerii Hiora Aug 01 '13 at 10:20
  • valerii, can you show some code? If you post a separate question and link the page here I'll see if I can help. – John Estropia Aug 01 '13 at 10:45
  • 4
    Hi, I'm having a problem with the code. The object which I'm trying to animate has other objects with constraints linked to it, and while those objects are moving as well, they are not doing so animatedly, they're just instantaneously moving to their final position. What do I do? – Fabio Aug 06 '13 at 11:07
  • 15
    Make sure you are calling and animating `layoutIfNeeded` of the topmost superview – John Estropia Sep 10 '13 at 10:13
  • thanks a lot. and it‘s also work like this // [UIView animateWithDuration:0.25f animations:^{ self.heightFromTop.constant = 550.0f; [myView setNeedsUpdateConstraints]; [myView layoutIfNeeded]; }]; – haiLong Jan 30 '15 at 01:55
  • Even when you have an image that resizes it's animate perfect – garanda Mar 13 '17 at 09:56
  • Why don't you move the constant change inside of the animation block ? I was confused a bit by that. In reality are we animating the constraint change. Not the constant change right ? This is why the constraint change is inside of the animation block. Changing the constant in the animation block does nothing. – 3366784 Aug 20 '17 at 04:48
  • Constraints are not directly animatable, layout frames are. You can prefer to put the constraint change inside the animation block but it will have no effect whatsoever because the animation block will only handle the frame changes in `layoutIfNeeded` – John Estropia Aug 21 '17 at 03:28
84

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

}];
Centurion
  • 14,106
  • 31
  • 105
  • 197
  • 1
    Thanks Centurion! This solution works better for me because I have other view properties needed to be animated along with the constraints. – Joseph Lin Jan 23 '14 at 23:03
  • Ran into some issue with this approach. I posted a slightly modified version that works for me as a separate answer. – Joseph Lin Jan 24 '14 at 01:03
  • This approach is better for me. Johns solution did wired animations of the UITableView's seperators. This one works totally solid. – Albert Schulz Oct 29 '14 at 15:35
14

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];
}];
Joseph Lin
  • 3,324
  • 1
  • 29
  • 39
7

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.

Scott Carter
  • 1,254
  • 9
  • 16
  • I think your view is jumping still because you are animating your views while another layout is animating them. If so, just pass `UIViewAnimationOptionLayoutSubviews` and/or `UIViewAnimationOptionBeginFromCurrentState` to the animation options. – John Estropia Nov 14 '13 at 08:11