I created a simple, one screen iOS app with only two controls, an activity indicator and a button, as shown below. When I run the code, I get the "starting" message, and 5 seconds later, the "stopping" message, but the activity indicator just displays without spinning. I must be missing something basic. This has had me stumped for weeks.
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBAction func startButton(_ sender: Any) {
activityIndicator.startAnimating()
print("starting")
sleep(5)
print("stopping")
activityIndicator.stopAnimating()
}
}
You are blocking the main thread and preventing any updates or interactions for 5 seconds. Screen does not refresh and in production your app would have been terminated by watchdog.
Try something like this:
@IBAction func startButton(_ sender: Any) {
let activityIndicator = self.activityIndicator
activityIndicator.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
activityIndicator.stopAnimating()
}
}
To explain on it there are multiple ways to skin this cat. But in general never block the main thread with anything. Either you have expensive operation or you are waiting for something you should do that on another thread.
Since you are just passing time there is no reason to use another thread. You can use a timer or dispatch queue. Timer would look like this:
@IBAction func startButton(_ sender: Any) {
let activityIndicator = self.activityIndicator
activityIndicator.startAnimating()
Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
activityIndicator.stopAnimating()
}
}
But if you wanted to use another thread it would mean switching to another thread, waiting for 5 seconds (you could use sleep) and then switching back to main thread to stop the animation. So quite unideal to do.
I hope this gives you some more understanding on the topic.
EDIT: From comment to replace the wait time with using CoreData to do some long lasting operations.
Core data has contexts. Each context represents an in-memory state of your database which is lazily loaded. To do heavy load you want to create a new background context, have it perform all of your logic, save to database and report back that it is done.
class DatabaseManager {
let container: NSPersistentContainer
func doHeavyLoad(onDone: (() -> Void)? = nil) {
let context = container.newBackgroundContext()
context.perform {
// Do all the stuff here
// Save context
try? context.save()
// Report is done on main
if let onDone {
DispatchQueue.main.async(execute: onDone)
}
}
}
}
This can be used with your logic like so:
@IBAction func startButton(_ sender: Any) {
let activityIndicator = self.activityIndicator
activityIndicator.startAnimating()
databaseManager.doHeavyLoad {
activityIndicator.stopAnimating()
}
}
It is important to see that intentionally a context already has a perform
method which will perform logic on the thread that context was designed to be on. It may be main thread or it might be some other thread. For that reason it is important to use DispatchQueue.main.async
to come back to main thread so that UI API can be called.
Also note that placing sleep
inside this perform
operation actually would wait for given time and then activity indicator would have been working correctly since the context is not performing the sleep on the main thread.