33

I would like to change the content of a UIButton to an ActivityIndicator after it is pressed.

I know buttons have an imageView and a titleLabel, but I don't know how to put an activity indicator in any of them.

This is how I create activity indicators:

let aiView = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
aiView.startAnimating()
aiView.center = CGPointMake(0,0)
aiView.hidesWhenStopped = false
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
jood
  • 2,188
  • 2
  • 21
  • 32

11 Answers11

57
import UIKit

class LoadingButton: UIButton {
private var originalButtonText: String?
var activityIndicator: UIActivityIndicatorView!

func showLoading() {
    originalButtonText = self.titleLabel?.text
    self.setTitle("", for: .normal)
    
    if (activityIndicator == nil) {
        activityIndicator = createActivityIndicator()
    }
    
    showSpinning()
}

func hideLoading() {
    self.setTitle(originalButtonText, for: .normal)
    activityIndicator.stopAnimating()
}

private func createActivityIndicator() -> UIActivityIndicatorView {
    let activityIndicator = UIActivityIndicatorView()
    activityIndicator.hidesWhenStopped = true
    activityIndicator.color = .lightGray
    return activityIndicator
}

private func showSpinning() {
    activityIndicator.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(activityIndicator)
    centerActivityIndicatorInButton()
    activityIndicator.startAnimating()
}

private func centerActivityIndicatorInButton() {
    let xCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: activityIndicator, attribute: .centerX, multiplier: 1, constant: 0)
    self.addConstraint(xCenterConstraint)
    
    let yCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: activityIndicator, attribute: .centerY, multiplier: 1, constant: 0)
    self.addConstraint(yCenterConstraint)
}
}
Joshua
  • 3,055
  • 3
  • 22
  • 37
Steve Rosenberg
  • 19,348
  • 7
  • 46
  • 53
  • For basic use that looks great. +1 – Nikhil Manapure Apr 08 '17 at 10:13
  • 2
    Remember to update the class of your button in storyboard to the custom class. E.g. Identity Inspector > Custom Class = LoadingButton. Otherwise you'll get `EXC_BAD_ACCESS` – Kilmazing Apr 25 '19 at 18:06
  • if your using IB, use NSAttributedString. Because IB sets the attibutedString. self.setAttributedTitle(NSAttributedString(string: ""), for: .normal) – GO.exe Nov 23 '20 at 08:53
  • I suggest add: `activityIndicator.isUserInteractionEnabled = false` .... otherwise touches on the activityIndicator won't execute the button's target. – nayooti Mar 05 '21 at 15:51
  • 3
    since iOS 15, you can use `button.configuration?.showsActivityIndicator = true` – Climbatize Jan 23 '22 at 01:37
29

@Boris: This should not be in an extension.

Here it is in swift 3/4, with improved code: disables button, works with images and titles.

class LoadingButton: UIButton {

    struct ButtonState {
        var state: UIControlState
        var title: String?
        var image: UIImage?
    }

    private (set) var buttonStates: [ButtonState] = []
    private lazy var activityIndicator: UIActivityIndicatorView = {
        let activityIndicator = UIActivityIndicatorView()
        activityIndicator.hidesWhenStopped = true
        activityIndicator.color = self.titleColor(for: .normal)
        self.addSubview(activityIndicator)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        let xCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: activityIndicator, attribute: .centerX, multiplier: 1, constant: 0)
        let yCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: activityIndicator, attribute: .centerY, multiplier: 1, constant: 0)
        self.addConstraints([xCenterConstraint, yCenterConstraint])
        return activityIndicator
    }()

    func showLoading() {
        activityIndicator.startAnimating()
        var buttonStates: [ButtonState] = []
        for state in [UIControlState.disabled] {
            let buttonState = ButtonState(state: state, title: title(for: state), image: image(for: state))
            buttonStates.append(buttonState)
            setTitle("", for: state)
            setImage(UIImage(), for: state)
        }
        self.buttonStates = buttonStates
        isEnabled = false
    }

    func hideLoading() {
        activityIndicator.stopAnimating()
        for buttonState in buttonStates {
            setTitle(buttonState.title, for: buttonState.state)
            setImage(buttonState.image, for: buttonState.state)
        }
        isEnabled = true
    }

}
Kurt J
  • 2,558
  • 24
  • 17
  • fyi, if you have a button in the storyboard you'd have to change its type inside the identity inspector not just in code. – iknowNothing Oct 12 '17 at 04:00
  • How do I use this LoadingButton, do I need to set the State, Title and Image on initialisation ? An example on its usage would be great. – SpaceX Mar 14 '18 at 00:09
18

Swift 4.0 with a little modification

class LoadingUIButton: UIButton {

    @IBInspectable var indicatorColor : UIColor = .lightGray

    var originalButtonText: String?
    var activityIndicator: UIActivityIndicatorView!

    func showLoading() {
        originalButtonText = self.titleLabel?.text
        self.setTitle("", for: .normal)

        if (activityIndicator == nil) {
            activityIndicator = createActivityIndicator()
        }

        showSpinning()
    }

    func hideLoading() {
        DispatchQueue.main.async(execute: {
            self.setTitle(self.originalButtonText, for: .normal)
            self.activityIndicator.stopAnimating()
        })
    }

    private func createActivityIndicator() -> UIActivityIndicatorView {
        let activityIndicator = UIActivityIndicatorView()
        activityIndicator.hidesWhenStopped = true
        activityIndicator.color = indicatorColor
        return activityIndicator
    }

    private func showSpinning() {
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(activityIndicator)
        centerActivityIndicatorInButton()
        activityIndicator.startAnimating()
    }

    private func centerActivityIndicatorInButton() {
        let xCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: activityIndicator, attribute: .centerX, multiplier: 1, constant: 0)
        self.addConstraint(xCenterConstraint)

        let yCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: activityIndicator, attribute: .centerY, multiplier: 1, constant: 0)
        self.addConstraint(yCenterConstraint)
    }

}
Carlos Mejía
  • 212
  • 4
  • 8
12

Swift 4:

extension UIButton {
    func loadingIndicator(_ show: Bool) {
        let tag = 808404
        if show {
            self.isEnabled = false
            self.alpha = 0.5
            let indicator = UIActivityIndicatorView()
            let buttonHeight = self.bounds.size.height
            let buttonWidth = self.bounds.size.width
            indicator.center = CGPoint(x: buttonWidth/2, y: buttonHeight/2)
            indicator.tag = tag
            self.addSubview(indicator)
            indicator.startAnimating()
        } else {
            self.isEnabled = true
            self.alpha = 1.0
            if let indicator = self.viewWithTag(tag) as? UIActivityIndicatorView {
                indicator.stopAnimating()
                indicator.removeFromSuperview()
            }
        }
    }
}

Usage: button.loadingIndicator(true/false)

Tieda Wei
  • 588
  • 5
  • 14
Kristian
  • 2,071
  • 23
  • 15
9

i have written a library in swift, it has 7 different styles and animations to show an indicatorview in uibutton, here it is

https://github.com/farshadjahanmanesh/loady

enter image description here

7

Swift 5, Xcode 11.3

I've modified it for my use case to include the button text, together with the spinner

import UIKit

class LoadingButton: UIButton {

var activityIndicator: UIActivityIndicatorView!

let activityIndicatorColor: UIColor = .gray

func loadIndicator(_ shouldShow: Bool) {
    if shouldShow {
        if (activityIndicator == nil) {
            activityIndicator = createActivityIndicator()
        }
        self.isEnabled = false
        self.alpha = 0.7
        showSpinning()
    } else {
        activityIndicator.stopAnimating()
        self.isEnabled = true
        self.alpha = 1.0
    }
}

private func createActivityIndicator() -> UIActivityIndicatorView {
    let activityIndicator = UIActivityIndicatorView()
    activityIndicator.hidesWhenStopped = true
    activityIndicator.color = activityIndicatorColor
    return activityIndicator
}

private func showSpinning() {
    activityIndicator.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(activityIndicator)
    positionActivityIndicatorInButton()
    activityIndicator.startAnimating()
}

private func positionActivityIndicatorInButton() {
    let trailingConstraint = NSLayoutConstraint(item: self,
                                               attribute: .trailing,
                                               relatedBy: .equal,
                                               toItem: activityIndicator,
                                               attribute: .trailing,
                                               multiplier: 1, constant: 16)
    self.addConstraint(trailingConstraint)

    let yCenterConstraint = NSLayoutConstraint(item: self,
                                               attribute: .centerY,
                                               relatedBy: .equal,
                                               toItem: activityIndicator,
                                               attribute: .centerY,
                                               multiplier: 1, constant: 0)
    self.addConstraint(yCenterConstraint)
}

}

If you're using storyboard, ensure that your button is of type LoadingButton (do this on your storyboard as well as view controller file, otherwise it'll crash)

@IBOutlet weak var myButton: LoadingButton!

and to use it,

myButton.loadIndicator(true)

Screenshot

Kelvin Fok
  • 621
  • 7
  • 9
4

Here is my edit with little less code.

class Button: UIButton {
    private var originalButtonText: String?

    private lazy var activityIndicator: UIActivityIndicatorView = {
        let activityIndicator = UIActivityIndicatorView()
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        activityIndicator.color = .black
        addSubview(activityIndicator)

        NSLayoutConstraint.activate([
            activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor),
            activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor)
        ])

        return activityIndicator
    }()

    func loading(_ isLoading: Bool) {
        isEnabled = !isLoading

        if isLoading {
            originalButtonText = titleLabel?.text
            setTitle("", for: .normal)
            activityIndicator.startAnimating()
        } else {
            setTitle(originalButtonText, for: .normal)
            activityIndicator.stopAnimating()
        }
    }
}

Usage: button.loading(true/false)

Denis Kakačka
  • 697
  • 1
  • 8
  • 21
2

Another solution for Swift4, I tried to make it simpler than previous solutions. This Extension allows you to resize the UIActivityIndicatorView (scale) to make fit inside the UIButton, and change the color.

https://gist.github.com/jalopezsuarez/0ecc885b3fd5c555630799a067d66d98

import Foundation
import UIKit

class UIButtonActivity: UIButton {

    @IBInspectable var indicatorColor : UIColor = .lightGray

    private var buttonLabel: String?

    func startAnimating() {
        self.isEnabled = false

        buttonLabel = self.titleLabel?.text
        self.setTitle("", for: .normal)

        let indicator = UIActivityIndicatorView()
        indicator.color = indicatorColor
        indicator.hidesWhenStopped = true

        let buttonHeight = self.bounds.size.height
        let buttonWidth = self.bounds.size.width
        indicator.center = CGPoint(x: buttonWidth/2, y: buttonHeight/2)

        let scale = max(min((self.frame.size.height - 4) / 21, 2.0), 0.0)
        let transform: CGAffineTransform = CGAffineTransform(scaleX: scale, y: scale)
        indicator.transform = transform

        self.addSubview(indicator)
        indicator.startAnimating()
    }

    func stopAnimating() {
        self.isEnabled = true

        if let titleLabel = buttonLabel {
            self.setTitle(titleLabel, for: .normal)
        }

        if let indicator = self.viewWithTag(tag) as? UIActivityIndicatorView {
            indicator.stopAnimating()
            indicator.removeFromSuperview()
        }
    }
}
jalopezsuarez
  • 414
  • 4
  • 13
  • This is great, but in stop animating you have viewWithTag, without a tag really being assigned. It might work for you but I had to manually assign a tag to the indicatorview for it to work. Cheers! – James Hall Oct 15 '18 at 21:03
1

Updated selected answer( steve-rosenberg ) for Apple Swift version 3.0.1 (swiftlang-800.0.58.6 clang-800.0.42.1) Xcode 8.1 (8B62)

import ObjectiveC

private var originalButtonText: String?
private var activityIndicator: UIActivityIndicatorView!

extension UIButton{

    func showLoading() {
        originalButtonText = self.titleLabel?.text
        self.setTitle("", for: .normal)

        if (activityIndicator == nil) {
            activityIndicator = createActivityIndicator()
        }

        showSpinning()
    }

    func hideLoading() {
        self.setTitle(originalButtonText, for: .normal)
        activityIndicator.stopAnimating()
    }

    private func createActivityIndicator() -> UIActivityIndicatorView {
        let activityIndicator = UIActivityIndicatorView()
        activityIndicator.hidesWhenStopped = true
        activityIndicator.color = UIColor.lightGray
        return activityIndicator
    }

    func showSpinning() {
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(activityIndicator)
        centerActivityIndicatorInButton()
        activityIndicator.startAnimating()
    }

    private func centerActivityIndicatorInButton() {
        let xCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: activityIndicator, attribute: .centerX, multiplier: 1, constant: 0)
        self.addConstraint(xCenterConstraint)

        let yCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: activityIndicator, attribute: .centerY, multiplier: 1, constant: 0)
        self.addConstraint(yCenterConstraint)
    }
}
Boris Nikolic
  • 746
  • 14
  • 24
1

Excellent @Steve,

Can use below implementation instead of line,

var activityIndicator: UIActivityIndicatorView!

use this, so that if we configure the activity indicator before calling showLoading(), the app wont crash.

private var _activityIndicator:UIActivityIndicatorView! = nil
var activityIndicator: UIActivityIndicatorView{
    set{
        _activityIndicator = newValue

    }
    get{
        if _activityIndicator == nil{
            _activityIndicator = createActivityIndicator()
        }
        return _activityIndicator
    }
}
infiniteLoop
  • 2,135
  • 1
  • 25
  • 29
0

Most of the answers are working with only title, in my case I need to manage attributed text and image as well,

import UIKit

class LoadingButton: UIButton {

    struct ButtonState {
        var state: UIControl.State
        var title: String?
        var attributedTitle: NSAttributedString?
        var image: UIImage?
    }

    private (set) var buttonStates: [ButtonState] = []

    private lazy var activityIndicator: UIActivityIndicatorView = {
        let activityIndicator = UIActivityIndicatorView()
        activityIndicator.hidesWhenStopped = true
        activityIndicator.color = self.titleColor(for: .normal)
        self.addSubview(activityIndicator)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        let xCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: activityIndicator, attribute: .centerX, multiplier: 1, constant: 0)
        let yCenterConstraint = NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: activityIndicator, attribute: .centerY, multiplier: 1, constant: 0)
        self.addConstraints([xCenterConstraint, yCenterConstraint])
        return activityIndicator
    }()

    func showLoading() {
        activityIndicator.color = self.titleColor(for: .normal)
        activityIndicator.startAnimating()
        var buttonStates: [ButtonState] = []
        for state in [UIControl.State.disabled] {
            let buttonState = ButtonState(state: state, title: title(for: state), attributedTitle: attributedTitle(for: state), image: image(for: state))
        buttonStates.append(buttonState)
            setTitle("", for: state)
            setAttributedTitle(NSAttributedString(string: ""), for: state)
            setImage(UIImage(), for: state)
        }
        self.buttonStates = buttonStates
        isEnabled = false
   }

   func hideLoading() {
       activityIndicator.stopAnimating()
       for buttonState in buttonStates {
           setTitle(buttonState.title, for: buttonState.state)
           setImage(buttonState.image, for: buttonState.state)
           setAttributedTitle(buttonState.attributedTitle, for: buttonState.state)
       }
       isEnabled = true
    }
}
Amalendu Kar
  • 458
  • 1
  • 6
  • 17