How to display an activity indicator or other UI before beginning a long task

It seems like at least once a week somebody asks why they can't display a UIActivityIndicatorView (or alert view), then perform a time-consuming synchronous task.

They are mystified that the activity indicator doesn't show up until after the time-consuming task is complete.

The reason it doesn't work is this: Cocoa queues up the user interface changes you make in your code, and applies them the next time your code returns and the application visits the event loop. So, if you do this:

The code fragments above assume that you have already created an activity indicator view in interface builder and hooked it up as an outlet called theActivityIndicator.

Note that the exact same issue comes up with any user interface change you want to make before doing a time-consuming task, and the same solution works. Just change the line that starts the activity indicator animating to whatever UI change you want to make.

Post edited by Duncan C on February 2013

Regards,
Duncan C
WareTo

Animated GIF created with Face Dancer, available for free in the app store.

// This line stops the activity indicator in the status bar
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

// This line stops the activity indicator on the view, in this case the table view
[activityIndicator stopAnimating];

}
- (void) autoResume
{
//perform time-consuming tasks
}

I don't think I've created an activity indicator through code before.

Your code looks reasonable. One thing I would change: Don't set the frame of your activity indicator. They're designed to draw at a fixed size. You're already using the center property to place it where you want it, so get rid of the assignment to the frame property.

I'd step through your code in the debugger and make sure you are getting back a valid activity indicator, and that it's hidden property is FALSE. Also make sure self.view is non-nil.

Regards,
Duncan C
WareTo

Animated GIF created with Face Dancer, available for free in the app store.

Why do you use [self performSelector:...afterdelay:0];instead of [self performSelectorInBackground:...]

The first method calling setNeedsDisplay hangs the view while the second does not since the first will perform setNeedsDisplay on the main thread. Is this reasoning correct?

Several things:

setNeedsDisplay is a UI call. You're not supposed to make UI calls from the background.

You can certainly use performSelectorInBackground, or one of the newer GCD calls, to do time-consuming tasks in the background, and that's often the better way to do things. (Using CGD is better than using performSelectorInBackground, because GCD lets the system manage a pool of background threads, where performSelectorInBackground always creates a new thread for each task you start, and creating threads is expensive and ties up memory.

If you do your time-consuming work in the background you might not even need an activity indicator at all. The UI won't freeze in that case.

Concurrent programming requires that you know what you are doing, however. There are a number of pitfalls that can get you in trouble.

The point of this thread was a quick-and-dirty fix for the problem of getting an activity indicator to animate before starting a time-consuming task on the main thread. Using GCD to do work in the background is a more advanced topic that is beyond the scope of this tutorial.

Regards,
Duncan C
WareTo

Animated GIF created with Face Dancer, available for free in the app store.

Only start the networkActivityIndicator when you are accessing the network for data and turn it off when network access has ended. Otherwise, you are giving your users false information of when you are actually using the network for data. Apple may reject your app if this is used incorrectly.

Another thought with the NetworkActivityIndicator is that it may be started by other processes in other queues. So, it would be a good idea to track how many processes are using the network with a counter and when they finish decrement the counter until you are back to zero to shut the indicator off.

You can certainly use performSelectorInBackground, or one of the newer GCD calls......Using GCD to do work in the background is a more advanced topic that is beyond the scope of this tutorial.

I've tried using GCD dispatch_async to call setNeedsDisplay by creating a custom dispatch_queue_t. However, when the action is run, the dispatch queue appears to pushed to the back and is not called for variable amounts of time ~5-10 seconds.

My code is currently using dispatch_async to first call a UIView background color change, then call setNeedsDisplay with another dispatch_async in the same custom queue. Both actions appear to be delayed. But if I use performselector to run setNeedsDisplay, there is no delay for the dispatch_async background color change called just before.

You can certainly use performSelectorInBackground, or one of the newer GCD calls......Using GCD to do work in the background is a more advanced topic that is beyond the scope of this tutorial.

I've tried using GCD dispatch_async to call setNeedsDisplay by creating a custom dispatch_queue_t. However, when the action is run, the dispatch queue appears to pushed to the back and is not called for variable amounts of time ~5-10 seconds.

My code is currently using dispatch_async to first call a UIView background color change, then call setNeedsDisplay with another dispatch_async in the same custom queue. Both actions appear to be delayed. But if I use performselector to run setNeedsDisplay, there is no delay for the dispatch_async background color change called just before.

You can't do UI code on a background thread. Therefore, your use of dispatch_async must be on the main thread, and it therefore still operates synchronously with the user interface. What's the point, then, of using GCD at all?

Regards,
Duncan C
WareTo

Animated GIF created with Face Dancer, available for free in the app store.

You can't do UI code on a background thread. Therefore, your use of dispatch_async must be on the main thread, and it therefore still operates synchronously with the user interface. What's the point, then, of using GCD at all?

Agreed. If you're using GCD or performSelectorInBackground: you need to make sure the background thread is doing the long (non-UI) task only. Although, you can do GUI updates from the BG thread via performSelectorOnMainThread. If your lucky and the GUI-affecting selector only has one or zero arguments you don't even need a wrapper.

Usually for long tasks to be nice to the user, I always want to implement a cancel button so blocking the main thread like this is a no-go, but if you can just sit and block, your original solution with afterDelay: looks pretty handy. If there is some magic way of having the best of both worlds (cancel+ main thread long task) please do tell.