304

I have a UITextField that I want to enlarge its width when tapped on. I set up the constraints and made sure the constraint on the left has the lower priority then the one that I am trying to animate on the right side.

Here is the code that I am trying to use.

// move the input box
UIView.animateWithDuration(10.5, animations: {
    self.nameInputConstraint.constant = 8
    }, completion: {
        (value: Bool) in
        println(">>> move const")
})

This works, but it seems to just happen instantly and there doesn't seem to be any movement. I tried to set it 10 seconds to make sure I wasn't missing anything, but I got the same results.

nameInputConstraint is the name of the constraint that I control dragged to connect into my class from IB.

user16217248
  • 3,119
  • 19
  • 19
  • 37
icekomo
  • 9,328
  • 7
  • 31
  • 59
  • 3
    possible duplicate of [How do I animate constraint changes?](http://stackoverflow.com/questions/12622424/how-do-i-animate-constraint-changes) – emem Sep 20 '15 at 16:33

7 Answers7

787

You need to first change the constraint and then animate the update.
This should be in the superview.

self.nameInputConstraint.constant = 8

Swift 2

UIView.animateWithDuration(0.5) {
    self.view.layoutIfNeeded()
}

Swift 3, 4, 5

UIView.animate(withDuration: 0.5) {
    self.view.layoutIfNeeded()
}
Mundi
  • 79,884
  • 17
  • 117
  • 140
  • 2
    Can someone explain how/why this works? Calling animationWithDuration on any UIView will animate any class that inherits from UIView and changes a physical value? – nipponese Mar 07 '16 at 00:53
  • 3
    The animation API you mention is used to animate the *properties* of views and layers. Here we need to animate the *change in layout*. That's what changing the constant of a layout constraint calls for - changing the constant alone does nothing. – Mundi Mar 07 '16 at 00:57
  • Thanks @ryan, for completing this answer with Swift 3.0. – Mundi Oct 10 '16 at 20:54
  • don't you think there's memory leak here, it should be UIView.animate(withDuration: 0.5) { [weak self] in self?.view.layoutIfNeeded() } – LiangWang Dec 30 '16 at 06:19
  • @Jacky Don't think so, this should be taken care of by the compiler. Did you measure a memory leak? – Mundi Dec 30 '16 at 14:43
  • @Mundi, honestly, i don't see memory leak. However, it seems that it does result into a cycle reference: self has captured closure and closure has captured self. so I guess my way would be better way even it does not actually make any leakage. – LiangWang Dec 30 '16 at 23:28
  • 3
    @Jacky Perhaps you are mistaking this with Objective-C reasoning in reference to strong and weak variables. Swift closures are different: once the closure finishes, there is no object holding on to the `self` used in the closure. – Mundi Dec 31 '16 at 11:42
  • This is causing unexpected behavior for me. My WKWebView scrolls to the top automatically. I'm assuming the scroll view scrolls to the top automatically when the layout is reset. I ended up not using this. Locking the scroll view won't work. – 3366784 Jun 22 '17 at 07:21
  • 3
    not working in my case ,it happening very first ,what to do – Shakti Jan 18 '18 at 07:12
  • 37
    It took me one hour to notice, that I have to call layoutIfNeeded on superview... – Nominalista Mar 30 '18 at 07:54
  • @Mundi, not really it depends on scopes. In UIView or DispatchQueue there is no need for capturing. – dimpiax Aug 18 '18 at 17:57
  • @Nominalista Is it necessary to call "self.view.layoutIfNeeded". For me it's working without calling it also. Not sure if its something new. Could anyone clarify on this ? – subin272 Jul 11 '19 at 10:33
51

SWIFT 4 and above:

self.mConstraint.constant = 100.0
UIView.animate(withDuration: 0.3) {
        self.view.layoutIfNeeded()
}

Example with completion:

self.mConstraint.constant = 100
UIView.animate(withDuration: 0.3, animations: {
        self.view.layoutIfNeeded()
    }, completion: {res in
        //Do something
})
Hadži Lazar Pešić
  • 1,031
  • 1
  • 13
  • 20
  • 3
    it's not working I have done this UIView.animate(withDuration: 10, delay: 0, options: .curveEaseInOut, animations: { self.leftViewHeightConstraint.constant = 200 self.leftView.layoutIfNeeded() }, completion: nil) – saurabhgoyal795 Apr 05 '18 at 08:24
  • 2
    You have to change the constraint and *then* animate. – JaredH May 24 '18 at 18:39
  • While this code snippet may be the solution, [including an explanation](https://meta.stackexchange.com/questions/114762/explaining-entirely-%E2%80%8C%E2%80%8Bcode-based-answers) really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – Narendra Jadhav Jun 14 '18 at 12:29
  • The above line Made my day :) Thank you @Hadzi – Nrv Feb 20 '20 at 04:39
34

It's very important to point out that view.layoutIfNeeded() applies to the view subviews only.

Therefore to animate the view constraint, it is important to call it on the view-to-animate superview as follows:

    topConstraint.constant = heightShift

    UIView.animate(withDuration: 0.3) {

        // request layout on the *superview*
        self.view.superview?.layoutIfNeeded()
    }

An example for a simple layout as follows:

class MyClass {

    /// Container view
    let container = UIView()
        /// View attached to container
        let view = UIView()

    /// Top constraint to animate
    var topConstraint = NSLayoutConstraint()


    /// Create the UI hierarchy and constraints
    func createUI() {
        container.addSubview(view)

        // Create the top constraint
        topConstraint = view.topAnchor.constraint(equalTo: container.topAnchor, constant: 0)


        view.translatesAutoresizingMaskIntoConstraints = false

        // Activate constaint(s)
        NSLayoutConstraint.activate([
           topConstraint,
        ])
    }

    /// Update view constraint with animation
    func updateConstraint(heightShift: CGFloat) {
        topConstraint.constant = heightShift

        UIView.animate(withDuration: 0.3) {

            // request layout on the *superview*
            self.view.superview?.layoutIfNeeded()
        }
    }
}
Stéphane de Luca
  • 12,745
  • 9
  • 57
  • 95
  • 1
    Updating a height constraint on Swift 4 and this worked while calling layoutIfNeeded on the view but not the superview did not. Really helpful, thanks! – Alekxos Nov 07 '18 at 01:15
  • 2
    This is really the correct answer. If you don't call it on superview, your view just jumps to the new location. This is a very important distinction. – Bryan Deemer May 29 '20 at 15:58
  • 2
    This should be a correct answer, I've spent whole night trying to fix it, thank you! At least tonight I will be able to sleep. – daxh Jun 03 '22 at 12:11
17

With Swift 5 and iOS 12.3, according to your needs, you may choose one of the 3 following ways in order to solve your problem.


#1. Using UIView's animate(withDuration:animations:) class method

animate(withDuration:animations:) has the following declaration:

Animate changes to one or more views using the specified duration.

class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void)

The Playground code below shows a possible implementation of animate(withDuration:animations:) in order to animate an Auto Layout constraint's constant change.

import UIKit
import PlaygroundSupport

class ViewController: UIViewController {

    let textView = UITextView()
    lazy var heightConstraint = textView.heightAnchor.constraint(equalToConstant: 50)

    override func viewDidLoad() {
        view.backgroundColor = .white
        view.addSubview(textView)

        textView.backgroundColor = .orange
        textView.isEditable = false
        textView.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.topAnchor.constraint(equalToSystemSpacingBelow: view.layoutMarginsGuide.topAnchor, multiplier: 1).isActive = true
        textView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor).isActive = true
        textView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor).isActive = true
        heightConstraint.isActive = true

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doIt(_:)))
        textView.addGestureRecognizer(tapGesture)
    }

    @objc func doIt(_ sender: UITapGestureRecognizer) {
        heightConstraint.constant = heightConstraint.constant == 50 ? 150 : 50
        UIView.animate(withDuration: 2) {
            self.view.layoutIfNeeded()
        }
    }

}

PlaygroundPage.current.liveView = ViewController()

#2. Using UIViewPropertyAnimator's init(duration:curve:animations:) initialiser and startAnimation() method

init(duration:curve:animations:) has the following declaration:

Initializes the animator with a built-in UIKit timing curve.

convenience init(duration: TimeInterval, curve: UIViewAnimationCurve, animations: (() -> Void)? = nil)

The Playground code below shows a possible implementation of init(duration:curve:animations:) and startAnimation() in order to animate an Auto Layout constraint's constant change.

import UIKit
import PlaygroundSupport

class ViewController: UIViewController {

    let textView = UITextView()
    lazy var heightConstraint = textView.heightAnchor.constraint(equalToConstant: 50)

    override func viewDidLoad() {
        view.backgroundColor = .white
        view.addSubview(textView)

        textView.backgroundColor = .orange
        textView.isEditable = false
        textView.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.topAnchor.constraint(equalToSystemSpacingBelow: view.layoutMarginsGuide.topAnchor, multiplier: 1).isActive = true
        textView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor).isActive = true
        textView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor).isActive = true
        heightConstraint.isActive = true

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doIt(_:)))
        textView.addGestureRecognizer(tapGesture)
    }

    @objc func doIt(_ sender: UITapGestureRecognizer) {
        heightConstraint.constant = heightConstraint.constant == 50 ? 150 : 50
        let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: {
            self.view.layoutIfNeeded()
        })
        animator.startAnimation()
    }

}

PlaygroundPage.current.liveView = ViewController()

#3. Using UIViewPropertyAnimator's runningPropertyAnimator(withDuration:delay:options:animations:completion:) class method

runningPropertyAnimator(withDuration:delay:options:animations:completion:) has the following declaration:

Creates and returns an animator object that begins running its animations immediately.

class func runningPropertyAnimator(withDuration duration: TimeInterval, delay: TimeInterval, options: UIViewAnimationOptions = [], animations: @escaping () -> Void, completion: ((UIViewAnimatingPosition) -> Void)? = nil) -> Self

The Playground code below shows a possible implementation of runningPropertyAnimator(withDuration:delay:options:animations:completion:) in order to animate an Auto Layout constraint's constant change.

import UIKit
import PlaygroundSupport

class ViewController: UIViewController {

    let textView = UITextView()
    lazy var heightConstraint = textView.heightAnchor.constraint(equalToConstant: 50)

    override func viewDidLoad() {
        view.backgroundColor = .white
        view.addSubview(textView)

        textView.backgroundColor = .orange
        textView.isEditable = false
        textView.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.topAnchor.constraint(equalToSystemSpacingBelow: view.layoutMarginsGuide.topAnchor, multiplier: 1).isActive = true
        textView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor).isActive = true
        textView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor).isActive = true
        heightConstraint.isActive = true

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doIt(_:)))
        textView.addGestureRecognizer(tapGesture)
    }

    @objc func doIt(_ sender: UITapGestureRecognizer) {
        heightConstraint.constant = heightConstraint.constant == 50 ? 150 : 50
        UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 2, delay: 0, options: [], animations: {
            self.view.layoutIfNeeded()
        })
    }

}

PlaygroundPage.current.liveView = ViewController()
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
  • 1
    Such a great answer! – Bashta Mar 14 '18 at 13:12
  • @Imanou great answer, appreciate the detail! Possibly add a short description of the options and what the difference is? Would be really helpful to me and I'm sure to others to have a sentence about why I would chose one over another. – rayepps Oct 03 '18 at 20:35
7

In my case, I only updated the custom view.

// DO NOT LIKE THIS
customView.layoutIfNeeded()    // Change to view.layoutIfNeeded()
UIView.animate(withDuration: 0.5) {
   customViewConstraint.constant = 100.0
   customView.layoutIfNeeded() // Change to view.layoutIfNeeded()
}
Den
  • 3,179
  • 29
  • 26
0

I would like to share my solution, in my not working case my constraint designed like below

view1WidthLayout = NSLayoutConstraint(item: view1,
                                      attribute: .width,
                                      relatedBy: .equal,
                                      toItem: nil,
                                      attribute: .width,
                                      multiplier: 1,
                                      constant: 20)

and whenever I tried to set constant before the animation like

view1WidthLayout.constant += 20

it set immediately so it did not work for me.

I change the definition of the constraint property like

view1WidthLayout = view1.widthAnchor.constraint(equalToConstant: 20)

then it worked for myself

eemrah
  • 1,603
  • 3
  • 19
  • 37
-1

Watch this.

The video says that you need to just add self.view.layoutIfNeeded() like the following:

UIView.animate(withDuration: 1.0, animations: {
       self.centerX.constant -= 75
       self.view.layoutIfNeeded()
}, completion: nil)
double-beep
  • 5,031
  • 17
  • 33
  • 41
mossman252
  • 462
  • 4
  • 3