Giovanni Palusa
Giovanni Palusa

Reputation: 1247

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)

      }
   }
}

Upvotes: 1

Views: 2893

Answers (5)

jithin
jithin

Reputation: 489

If someone looking for Swift UI code [SWIFT 5].

enter image description here

import SwiftUI
struct ButtonContentView: View {
    @State private var isButtonPressed = false
    @State private var isLoading = false
    
    var body: some View {
        ZStack {
            Button(action: {
                withAnimation {
                    isButtonPressed.toggle()
                    isLoading.toggle()
                }
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    withAnimation {
                        isButtonPressed.toggle()
                        isLoading.toggle()
                    }
                }
            }) {
                withAnimation(.easeInOut) {
                    Text(isButtonPressed ? "" : "Press Me")
                        .foregroundColor(.white)
                        .frame(width: isButtonPressed ? 60 : 200, height: isButtonPressed ? 60 : 50)
                        .background(Color.blue)
                        .clipShape(isButtonPressed ? RoundedRectangle(cornerRadius: 50) : RoundedRectangle(cornerRadius: 10))
                }
                
            }
            
            if isLoading {
                CircularProgressView()
                    .opacity(isButtonPressed ? 1 : 0)
                    .scaleEffect(isButtonPressed ? 1 : 0)
            }
        }
    }
}

struct CircularProgressView: View {
    @State private var rotation = 0.0
    var body: some View {
        Circle()
            .trim(from: 0.08, to: 1.0)
            .stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
            .foregroundColor(.white)
            .rotationEffect(.degrees(rotation))
            .frame(width: 40, height: 40)
        
            .onAppear {
                DispatchQueue.main.async {
                    withAnimation(.linear(duration: 1)
                        .repeatForever(autoreverses: false)) {
                            rotation = 360.0
                        }
                }
            }
    }
}

struct ButtonContentView_previews: PreviewProvider {
    static var previews: some View {
        ButtonContentView()
    }
}

Upvotes: 0

orazz
orazz

Reputation: 2198

So I did some changes and it works perfect for me. And I set base constraints for button in interface builder.

// StartAnimation    
self.payButton.startAnimating(originalConstraints: sender.constraints)
// StopAnimation    
self.payButton.startAnimating(false, originalConstraints: sender.constraints)    
    
func startAnimating(_ showLoading: Bool = true, originalConstraints: [NSLayoutConstraint]) {
                lazy var activityIndicator: UIActivityIndicatorView = {
                    let activityIndicator = UIActivityIndicatorView()
                    activityIndicator.translatesAutoresizingMaskIntoConstraints = false
                    activityIndicator.isUserInteractionEnabled = false
                    activityIndicator.color = .white
                    activityIndicator.startAnimating()
                    activityIndicator.alpha = 0
                    return activityIndicator
                }()
        
                let spinnerConst = [
                    activityIndicator.widthAnchor.constraint(equalToConstant: 40.0),
                    activityIndicator.heightAnchor.constraint(equalToConstant: 40.0),
                    activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor)
                ]
        
                let buttonConst = [
                    self.widthAnchor.constraint(equalToConstant: 40.0),
                    self.heightAnchor.constraint(equalToConstant: 40.0)
                ]
        
                if showLoading {
                    NSLayoutConstraint.deactivate(originalConstraints)
        
                    self.addSubview(activityIndicator)
                    self.superview?.addConstraints(buttonConst)
                    self.addConstraints(buttonConst)
                    self.addConstraints(spinnerConst)
        
                    UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
                        self.layer.cornerRadius = 20.0
                        activityIndicator.alpha = 1
                        self.titleLabel?.alpha = 0
                        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(spinnerConst)
                        self.removeConstraints(buttonConst)
                        self.superview?.removeConstraints(buttonConst)
                        self.superview?.addConstraints(originalConstraints)
                        self.addConstraints(originalConstraints)
                        NSLayoutConstraint.activate(originalConstraints)
                        self.titleLabel?.alpha = 1
                        self.layer.cornerRadius = 6
                        self.layoutIfNeeded()
                    }, completion: nil)
                }
            }

Upvotes: 0

Yadav-JI
Yadav-JI

Reputation: 1

I am updating code to set dynamic button height and manage the circle according to button parent height. You can use

button.frame.height

to make a circle.

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: self.frame.height),
        NSLayoutConstraint(item: spinner, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: self.frame.height)
    ]

    // Constrains which will add in button
    let selfCostraints = [
        NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: self.frame.height),
        NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: self.frame.height),
    ]

    // 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 = self.frame.height / 2
            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)
    }
}

}

Upvotes: 0

Sagar Chauhan
Sagar Chauhan

Reputation: 5823

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

Upvotes: 3

Robin Stewart
Robin Stewart

Reputation: 3903

I noticed you set self.translatesAutoresizingMaskIntoConstraints = false in the first animation but you didn't set it back to true in your second animation.

That may be a source of the problem: you need to set self.translatesAutoresizingMaskIntoConstraints = true during your second animation.

It will probably be less confusing if you turn off translatesAutoresizingMaskIntoConstraints in Interface Builder (or wherever you create the button initially), and do all your layout with normal constraints.

Upvotes: 0

Related Questions