Search code examples
swiftuiviewnslayoutconstraint

Swift - Animate Constraints in Custom UIView


I am trying to animate constraints in a custom UIView. The ending layout is correct, but I cannot get UIView.animate to work correctly, and I have seen that everyone is calling self.view.layoutIfNeeded() from a view controller, so perhaps that is my problem. I am calling self.layoutIfNeeded() on the custom view itself. In case the hierarchy matters, I have as follows: ViewController > TableView > Cell > View > AnimationView.

class AnimationView: UIView {
    
    // MARK: - Properties
    let ratioMultiplier: CGFloat = 0.28
    var listLeadingConstraint: NSLayoutConstraint!
    var listTrailingConstraint: NSLayoutConstraint!
    var checkmarkLeadingConstraint: NSLayoutConstraint!
    var checkmarkTrailingConstraint: NSLayoutConstraint!
    
    // MARK: - UI Components
    lazy var listImageView: UIImageView = {
        let image = UIImage(named: "list.small")
        let result = UIImageView(image: image)
        return result
    }()
    
    lazy var checkmarkImageView: UIImageView = {
        let image = UIImage(named: "checkmark.small")
        let result = UIImageView(image: image)
        return result
    }()
    
    // MARK: - Inits
    init() {
        super.init(frame: .zero)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        return nil
    }
    
    // MARK: - Methods
    func playAnimation() {
        // 1. change constraints
        listLeadingConstraint.constant += (listImageView.frame.size.width * ratioMultiplier)
        listTrailingConstraint.constant += (listImageView.frame.size.width * ratioMultiplier)
        checkmarkLeadingConstraint.constant += (checkmarkImageView.frame.size.width * ratioMultiplier)
        checkmarkTrailingConstraint.constant += (checkmarkImageView.frame.size.width * ratioMultiplier)
        self.layoutIfNeeded()
        
        // 2. animate constraints
        UIView.animate(withDuration: 3.0) {
            self.layoutIfNeeded()
        }
    }
    
    func setupView() {
        // 1. add subviews
        addSubview(listImageView)
        addSubview(checkmarkImageView)
        
        // 2. add constraints       
        ConstraintHelper.anchor(view: checkmarkImageView, options: [.top, .bottom])
        checkmarkLeadingConstraint = checkmarkImageView.leadingAnchor.constraint(equalTo: leadingAnchor)
        checkmarkTrailingConstraint = checkmarkImageView.trailingAnchor.constraint(equalTo: trailingAnchor)
        checkmarkLeadingConstraint.constant -= checkmarkImageView.frame.size.width * ratioMultiplier
        checkmarkTrailingConstraint.constant -= checkmarkImageView.frame.size.width * ratioMultiplier
        NSLayoutConstraint.activate([checkmarkLeadingConstraint, checkmarkTrailingConstraint])
        
        ConstraintHelper.anchor(view: listImageView, options: [.top, .bottom])
        listLeadingConstraint = listImageView.leadingAnchor.constraint(equalTo: leadingAnchor)
        listTrailingConstraint = listImageView.trailingAnchor.constraint(equalTo: trailingAnchor)
        NSLayoutConstraint.activate([listLeadingConstraint, listTrailingConstraint])
    }
}

Additional info: playAnimation() is called from cellForRow on the table view and I also tried using self.superview?.layoutIfNeeded() calls to no success. The ConstraintHelper calls translatesAutoresizingMaskIntoConstraints and the constraints are working correctly, so don't worry about that. Any help would be great.


Solution

  • The reason why your initial post is not working is that you called your first layoutIfNeeded() after the constraint changes. When your animation block is executed, there's no changes to be animated at all.

    It does not matter whether you change the constraints in or out of the animation block.

    The common steps are:

    1. Call layoutIfNeeded() on the rootest view that your constraints may affect. In your case, self is ok because all the involving constraints are for the subviews. In some cases, you might change a constraint for the self view, then you need to call layoutIfNeeded on its superview.
    2. Make your constraint changes: modify constant property, activate/deactivate constraints, etc.
    3. Call layoutIfNeeded() in a animation block on the same view as you did in the first step.

    Please try the following alternative:

    func playAnimation() {
        // Request a layout to clean pending changes for the view if there's any.
        self.layoutIfNeeded()
        
        // Make your constraint changes.
        listLeadingConstraint.constant += (listImageView.frame.size.width * ratioMultiplier)
        listTrailingConstraint.constant += (listImageView.frame.size.width * ratioMultiplier)
        checkmarkLeadingConstraint.constant += (checkmarkImageView.frame.size.width * ratioMultiplier)
        checkmarkTrailingConstraint.constant += (checkmarkImageView.frame.size.width * ratioMultiplier)
        
        UIView.animate(withDuration: 3.0) {
            // Let your constraint changes update right now.
            self.layoutIfNeeded()
        }
    }