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)
}
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
}
}
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
}
}
}
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.