iosswiftanimationuibuttonuianimation

Animating button to present a spinner in Swift


So I want to make a button animate on press to go to a circle, and then be able to send the button back to its original state. This is my current animation, and as you can see I'm halfway there.

GIF showing how the animation is currently working

As you also can see I'm having multiple issues here. First of all, when I set my new constraints the X constraint doesn't place the circle in the middle of the parent view. And then my initial thought was that when i call the reset function, that I would also pass the original constraints of the view, but that just isn't working.

My idea is that when i'm using it i'll put a UIView and then have the button inside that view, so I can manipulate the constraints of it. This would also be the case if i'm putting a button in a UIStackView.

Here's my code, any input would be awesome

extension UIButton {

func animateWhileAwaitingResponse(showLoading: Bool, originalConstraints: [NSLayoutConstraint]) {

    let spinner = UIActivityIndicatorView()
    let constraints = [
        NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: self.superview, attribute: .centerX, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 45),
        NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 45),
        NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: self.superview, attribute: .top, multiplier: 1, constant: 4),
        NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: self.superview, attribute: .bottom, multiplier: 1, constant: 8),
        NSLayoutConstraint(item: spinner, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: spinner, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: spinner, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 45),
        NSLayoutConstraint(item: spinner, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 45)
    ]

    if showLoading {

        NSLayoutConstraint.deactivate(self.constraints)
        self.translatesAutoresizingMaskIntoConstraints = false
        spinner.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(spinner)
        self.superview?.addConstraints(constraints)
        spinner.color = .white
        spinner.startAnimating()

        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
            self.setTitleColor(.clear, for: .normal)
            self.layer.cornerRadius = 22.5
            self.layoutIfNeeded()
        }, completion: nil)
    } else {
        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
            NSLayoutConstraint.deactivate(self.constraints)
            self.setTitleColor(.white, for: .normal)
            self.superview?.addConstraints(originalConstraints)
            NSLayoutConstraint.activate(originalConstraints)
            self.layer.cornerRadius = 0

            for subview in self.subviews where subview is UIActivityIndicatorView {
                subview.removeFromSuperview()
            }
            self.layoutIfNeeded()
        }, completion: nil)

      }
   }
}

Solution

  • I have updated your button extension code as follow, which is adding and removing constraints with animation.

    extension UIButton {
    
        func animateWhileAwaitingResponse(showLoading: Bool, originalConstraints: [NSLayoutConstraint]) {
    
            let spinner = UIActivityIndicatorView()
            spinner.isUserInteractionEnabled = false
    
            // Constraints which will add in supper view
            let constraints = [
                NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: self.superview, attribute: .centerX, multiplier: 1, constant: 0),
                NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: self.superview, attribute: .centerY, multiplier: 1, constant: 0),
    
                NSLayoutConstraint(item: spinner, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0),
                NSLayoutConstraint(item: spinner, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0),
                NSLayoutConstraint(item: spinner, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 45),
                NSLayoutConstraint(item: spinner, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 45)
            ]
    
            // Constrains which will add in button
            let selfCostraints = [
                NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 45),
                NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 45),
            ]
    
            // Keeping this outside of condition due to adding constrains programatically.
            self.translatesAutoresizingMaskIntoConstraints = false
            spinner.translatesAutoresizingMaskIntoConstraints = false
    
            if showLoading {
    
                // Remove width constrains of button from superview
                // Identifier given in storyboard constrains
                self.superview?.constraints.forEach({ (constraint) in
                    if constraint.identifier == "buttonWidth" {
                        constraint.isActive = false
                    }
                })
    
                NSLayoutConstraint.deactivate(self.constraints)
    
                self.addSubview(spinner)
                self.superview?.addConstraints(constraints)
                self.addConstraints(selfCostraints)
                spinner.color = .white
                spinner.startAnimating()
                spinner.alpha = 0
    
                UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
                    self.setTitleColor(.clear, for: .normal)
                    self.layer.cornerRadius = 22.5
                    spinner.alpha = 1
                    self.layoutIfNeeded()
                }, completion: nil)
    
            } else {
    
    
                UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
    
                    for subview in self.subviews where subview is UIActivityIndicatorView {
                        subview.removeFromSuperview()
                    }
    
                    self.removeConstraints(selfCostraints)
                    NSLayoutConstraint.deactivate(self.constraints)
                    self.setTitleColor(.white, for: .normal)
                    self.superview?.addConstraints(originalConstraints)
                    NSLayoutConstraint.activate(originalConstraints)
                    self.layer.cornerRadius = 0
    
                    self.layoutIfNeeded()
                }, completion: nil)
            }
        }
    } 
    

    I have added following constrains to button:

    Button Constrains

    Also, added identifier of button's width constraint to remove from super which will add runtime from original constrains.

    Width Constrains

    Then I have change width of button programatically by taking outlet of width constrains:

    @IBOutlet weak var const_btnAnimation_width : NSLayoutConstraint!
    

    inside viewDidLoad method

    self.const_btnAnimation_width.constant = UIScreen.main.bounds.width - 40
    

    where 40 is sum of leading and trailing space.

    on button click

    @IBAction func btnAnimationPressed(_ sender: UIButton) {
    
        sender.isSelected = !sender.isSelected
    
        if sender.isSelected {
            self.btnAnimation.animateWhileAwaitingResponse(showLoading: true, originalConstraints: sender.constraints)
        } else {
            self.btnAnimation.animateWhileAwaitingResponse(showLoading: false, originalConstraints: self.btnAnimationConstraints)
        }
    }
    

    btnAnimationConstraints is array of NSLayoutConstraint as follow:

    var btnAnimationConstraints = [NSLayoutConstraint]()
    

    So I just assign all constrains of button inside viewDidAppear method as follow:

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.btnAnimationConstraints = self.btnAnimation.constraints
    }
    

    I hope this will help you.

    Output:

    Button Animation