skip to Main Content

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

2

Answers


  1. Chosen as BEST ANSWER

    In my code, I'm now using the DispatchQueue instead of calling "rescoreAllDecks" directly:

    // rescore all decks in background
    activityIndicator.startAnimating()
    DispatchQueue.global(qos: .background).async {
        self.mydelegate.deckClass.rescoreAllDecks()
        self.stopAnimating()
    }
    

    I also created a separate function to stop the spinner:

    func stopAnimating() {
        DispatchQueue.main.async {
            self.activityIndicator.stopAnimating()
        }
    }
    

    This does solves the problem. I am going to take a note of this solution for future reference. Thanks again.


  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search