Search code examples
swiftanimationnslayoutconstraint

why NSLayoutConstraint animation works only one time?


in my project I have a someView(of type UIView), that inside holderView(of type UIView). someView have 2 state. in state 1 the someView become large and in the step 2 the someView become small. when some condition where right, someView show in state 1 and when it's not someView show in state 2. I want to do this with animation and I use NSLayoutConstraint in my project. I'm using this codes(someViewHeight and someViewWidth are of type NSLayoutConstraint):

func minimizeSomeView() {
    someViewHeight.constant = holderView.frame.width
    someViewWidth.constant  = holderView.frame.height

    UIView.animate(withDuration: 1.0) {
        self.view.layoutIfNeeded()
    }
}

func maximizeSomeView() {
    someViewHeight.constant = holderView.frame.width/4
    someViewWidth.constant  = holderView.frame.height/4

    UIView.animate(withDuration: 1.0) {
        self.view.layoutIfNeeded()
    }
}

if someTextField.text != nil {
    minimizeSomeView()
} else. {
    maximizeSomeView()
}

I define someViewHeight and someViewWidth inside viewDidLayoutSubviews(), this is my codes:

class ViewController: UIViewController {

    private var holderView, someView: UIView!
    private var someViewHeight,someViewWidth: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()
    
        super.viewDidLoad()
    
        view.backgroundColor = .white
    
        // Holder View
        holderView = UIView()
        holderView.backgroundColor = .red
        holderView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(holderView)
    
        NSLayoutConstraint.activate([
            holderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            holderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            holderView.topAnchor.constraint(equalTo: view.topAnchor, constant: 120),
            holderView.heightAnchor.constraint(equalToConstant: view.frame.height * 40 / 100)
        ])
    
        // Some View
        someView = UIView()
        someView.backgroundColor = .blue
        someView.translatesAutoresizingMaskIntoConstraints = false
        holderView.addSubview(someView)
    
        someView.topAnchor.constraint(equalTo: holderView.topAnchor).isActive = true
        someView.trailingAnchor.constraint(equalTo: holderView.trailingAnchor).isActive = true
    
        // Some TextField
        let someTextField = UITextField()
        someTextField.backgroundColor = .yellow
        someTextField.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(someTextField)
    
        NSLayoutConstraint.activate([
            someTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            someTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            someTextField.topAnchor.constraint(equalTo: view.bottomAnchor, constant: -100),
            someTextField.heightAnchor.constraint(equalToConstant: 50)
        ])
    
        someTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
    }


    override func viewDidLayoutSubviews() {
    
        someViewHeight = someView.heightAnchor.constraint(equalToConstant: holderView.frame.width)
        someViewHeight.isActive = true
        someViewWidth = someView.widthAnchor.constraint(equalToConstant: holderView.frame.width)
        someViewWidth.isActive = true
    }

    func minimizeSomeView() {
    
        someViewHeight.constant = holderView.frame.width/4
        someViewWidth.constant  = holderView.frame.height/4
    
        UIView.animate(withDuration: 1.0) {
            self.view.layoutIfNeeded()
        }
    }

    func maximizeSomeView() {
    
        someViewHeight.constant = holderView.frame.width
        someViewWidth.constant  = holderView.frame.height
    
        UIView.animate(withDuration: 1.0) {
            self.view.layoutIfNeeded()
        }
    }

    @objc func textFieldDidChange(_ textfield: UITextField) {

        if textfield.text!.count > 0 {
            self.minimizeSomeView()
        } else {
            self.maximizeSomeView()
        }
    }
}

Solution

  • You should not update the constraints in viewDidLayoutSubviews, specifically when you are using auto layout. You don't need this block

    When you add constraint you can initialize and make them active. Here we can specify the constraint in proportion. i.e Height of someView is 0.4 of the holderView. Similarly, Width of someView is 0.4 of the holderView.

    1. To fix the issue , you can perform below changes

    // Your some View Constraints

        someView.topAnchor.constraint(equalTo: holderView.topAnchor).isActive = true
        someView.trailingAnchor.constraint(equalTo: holderView.trailingAnchor).isActive = true
        maximizeSomeView()
    
    1. Remove the viewDidLayouSubView Code

    2. Update your maximize and minimize events. Here I have to remove the existing constraints as .multiplier is a readonly property.

    func  minimizeSomeView() {
    
    removeExistingConstriant()
    
    UIView.animate(withDuration: 1.0) { [unowned self] in
    
    self.someViewWidth = self.someView.widthAnchor.constraint(equalTo: self.holderView.widthAnchor, multiplier: 1.0)
    
    self.someViewWidth.isActive = true
    
    self.someViewHeight = self.someView.heightAnchor.constraint(equalTo:
    self.holderView.heightAnchor, multiplier: 1.0)
    
    self.someViewHeight.isActive = true
    
    self.view.layoutIfNeeded()
    
    }
    
    }
    
    func  maximizeSomeView() {
    
    removeExistingConstriant()
    
    UIView.animate(withDuration: 1.0) { [unowned self] in
    
    self.someViewWidth = self.someView.widthAnchor.constraint(equalTo: self.holderView.widthAnchor, multiplier: 0.4)
    
    self.someViewWidth.isActive = true
    
    self.someViewHeight = self.someView.heightAnchor.constraint(equalTo:
    self.holderView.heightAnchor, multiplier: 0.4)
    
    self.someViewHeight.isActive = true
    
    self.view.layoutIfNeeded()
    
    }
    
    }
    
    func  removeExistingConstriant(){
    
    if  self.someViewHeight  !=  nil {
    
    self.someViewHeight.isActive = false
    
    }
    
    if  self.someViewWidth  !=  nil {
    
    self.someViewWidth.isActive = false
    
    }
    
    }