Start and stop activity indicator swift3

Ana picture Ana · Nov 17, 2016 · Viewed 14.3k times · Source

I am learning Swift and now working on my home task. One of my buttons calls a function where I reload a collection view and it takes time. I am trying to implement an activity indicator to show the process. The problem that if I start the indicator, load the collection view, and then stop the indicator within the button action, the indicator doesn't appear. What would be the correct way to implement the indicator in this case? Here is the piece of the code:

let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.whiteLarge)
@IBOutlet weak var onCIFilterView: UICollectionView!
@IBOutlet var cifilterMenu: UIView!

@IBAction func onCIFiltersButton(_ sender: UIButton) {
    if (sender.isSelected) {
        hideCIFilters()
        sender.isSelected = false
    }
    else {
        activityIndicator.startAnimating()
        showCIFilters()  //the menu with UIView collection
        activityIndicator.stopAnimating()
        sender.isSelected = true
    }
}

func showCIFilters () {
    onCIFilterView.reloadData() // reloading collection view
    view.addSubview (cifilterMenu)
    let bottomConstraint = cifilterMenu.bottomAnchor.constraint(equalTo: mainMenu.topAnchor)
    let leftConstraint = cifilterMenu.leftAnchor.constraint(equalTo: view.leftAnchor)
    let rightConstraint = cifilterMenu.rightAnchor.constraint(equalTo: view.rightAnchor)
    let heightConstraint = cifilterMenu.heightAnchor.constraint(equalToConstant: 80)
    NSLayoutConstraint.activate([bottomConstraint, leftConstraint, rightConstraint, heightConstraint])
    view.layoutIfNeeded()
}

func hideCIFilters () {
    currentImage = myImage.image
    cifilterMenu.removeFromSuperview()

}

override func viewDidLoad() {
    super.viewDidLoad()
    //.....
    activityIndicator.center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2)
    activityIndicator.color = UIColor.lightGray
    view.addSubview(activityIndicator)
}

Answer

Duncan C picture Duncan C · Nov 17, 2016

The problem you are facing is that UI changes like starting an activity indicator don't take place until your code returns and the app serves the event loop.

Your code is synchronous, so the activity indicator never gets called.

You need to start the activity indicator spinning, return, and then do your time-consuming activity after a delay. You can use dispatch_after for this (or DispatchQueue.main.asyncAfter in Swift 3).

Here is an example IBAction that runs an activity indicator for 2 seconds after tapping a button:

@IBAction func handleButton(_ sender: UIButton) {
  activityIndicator.startAnimating()
  sender.isEnabled = false
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {

    // your time-consuming code here
    sleep(2) //Do NOT use sleep on the main thread in real code!!!
    self.activityIndicator.stopAnimating()
    sender.isEnabled = true
  }

}

Edit:

Your code would look like this:

@IBAction func onCIFiltersButton(_ sender: UIButton) {
    if (sender.isSelected) {
        hideCIFilters()
        sender.isSelected = false
    }
    else {
        activityIndicator.startAnimating()
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
            showCIFilters()  //the menu with UIView collection
            activityIndicator.stopAnimating()
            sender.isSelected = true
        }
    }
}

Edit #2:

Again, UI changes don't actually take place until your code RETURNS and the app services the event loop

If you have code like this:

activityIndicator.startAnimating()
sleep(2)  //This is in place of your time-consuming synchronous code.
activityIndicator.stopAnimating()

Then what happens is this:

You queue up a call to start the activity indicator the next time your code returns. (So it doesn't happen yet.)

You block the main thread with some time-consuming task. While this is going on, your activity indicator has not started spinning.

Finally your time consuming task completes. Now your code calls activityIndicator.stopAnimating(). Finally, your code returns, your app services the event loop, and the calls to start and then immediately stop the activity indicator get called. As a result the activity indicator does not actually spin.

This code does it differently:

//Queue up a call to start the activity indicator
activityIndicator.startAnimating()

//Queue up the code in the braces to be called after a delay.
//This call returns immediately, before the code in the braces is run.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
    showCIFilters()  //the menu with UIView collection
    activityIndicator.stopAnimating()
    sender.isSelected = true
}

Since closure passed into the call to DispatchQueue.main.asyncAfter is not run right away, that call returns immediately.

The app returns to service the event loop, and the activity indicator starts spinning. Shortly after that, Grand Central Dispatch invokes the block of code that you passed to it, which begins running your time-consuming operation (on the main thread) and then makes a cal to stop the activity indicator once the time-consuming task has completed.