Advertise here




Advertise here

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

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

Duncan CDuncan C Posts: 9,114Tutorial Authors, Registered Users @ @ @ @ @ @ @
edited February 2013 in iPhone SDK Tutorials
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:

<ol>
<li>start activity indicator</li>
<li>do time-consuming work</li>
<li>stop activity indicator</li>
<li>return</li>
</ol>


Then the activity indicator doesn't actually display at all. The UI changes don't take place until after your code returns, and by then, the time-consuming work is over.

The key to fixing this is a method called performSelector:withObject:afterDelay:. That method lets you invoke a method in the future.

What you do is this:

Split out your time-consuming code into a separate method. Let's call the method doSomethingSlow.

<CODE>- (IBAction) someMethod
{
[theActivityIndicator startAnimating]; //Or whatever UI Change you need to make
[self performSelector: @selector(doSomethingSlow)
withObject: nil
afterDelay: 0];
return;
}

- (void) doSomethingSlow
{
//perform time-consuming tasks
[theActivityIndicator stopAnimating]; //Or whatever step to indicate that the task is done.
}
</CODE>


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
Regards,
Duncan C
WareTo

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

I'm available for one-on-one help at CodeMentor

Replies

  • StormdogStormdog Posts: 7New Users Noob
    edited September 2011
    I followed this technique, and still do not see the indicator… ??

    I am creating my indicator view programmatically, is that the problem?

    UIActivityIndicatorView *activityIndicator;
    activityIndicator = [[[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray] autorelease];
    activityIndicator.frame = CGRectMake(0.0, 0.0, 40.0, 40.0);
    activityIndicator.center = self.view.center;
    [self.view addSubview: activityIndicator];


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

    // This line starts the activity indicator on the view
    [activityIndicator startAnimating];


    [self performSelector: @selector(autoResume)
    withObject: nil
    afterDelay: 0];

    // 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
    }
  • Duncan CDuncan C Posts: 9,114Tutorial Authors, Registered Users @ @ @ @ @ @ @
    edited September 2011
    Stormdog wrote: »
    I followed this technique, and still do not see the indicator… ??

    I am creating my indicator view programmatically, is that the problem?

    UIActivityIndicatorView *activityIndicator;
    activityIndicator = [[[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray] autorelease];
    activityIndicator.frame = CGRectMake(0.0, 0.0, 40.0, 40.0);
    activityIndicator.center = self.view.center;
    [self.view addSubview: activityIndicator];


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

    // This line starts the activity indicator on the view
    [activityIndicator startAnimating];


    [self performSelector: @selector(autoResume)
    withObject: nil
    afterDelay: 0];

    // 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

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

    I'm available for one-on-one help at CodeMentor
  • StormdogStormdog Posts: 7New Users Noob
    edited September 2011
    Verified above as suggested, all good; but, still NO JOY

    Does it matter what kind of view/controller this is being pushed upon?
  • ansonlansonl Posts: 255Registered Users @ @
    edited February 2013
    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?
    Look...<br />
    <a href="http://apparentet.ch"; target="_blank">[SIGPIC][/SIGPIC]<br />
    Apparent Etch</a>
  • Duncan CDuncan C Posts: 9,114Tutorial Authors, Registered Users @ @ @ @ @ @ @
    ansonl said:

    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

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

    I'm available for one-on-one help at CodeMentor
  • GHuebnerGHuebner Posts: 665Registered Users @ @ @
    edited February 2013
    I know using GCD is a more advanced topic but the base code to start the spinner, perform the process that will take a long time, and then stop the spinner could follow a pretty generic pattern.

    Start the spinner, create your queue and dispatch your queue async and put your processing code in the block and then within that block, dispatch work to the main queue to update the UIKit components.
    
    -(void)useGCD;
    {
        // Start your spinner animating
        [self.activitySpinner startAnimating];
        
        // Create your dispatch queue
        dispatch_queue_t myNewQueue = dispatch_queue_create("my Queue", NULL);
        
        // Dispatch work to your queue
        dispatch_async(myNewQueue, ^{
        
                // Perform long activity here.
     
            // Dispatch work back to the main queue for your UIKit changes
            dispatch_async(dispatch_get_main_queue(), ^{
                
                // update your UI here from your changes.
                [self.activitySpinner stopAnimating];
            
            });
        });
    }
    
    I would caution also using:
    
    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
    
    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.
    Post edited by GHuebner on
  • ansonlansonl Posts: 255Registered Users @ @
    edited March 2013
    Duncan C said:


    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.
    Look...<br />
    <a href="http://apparentet.ch"; target="_blank">[SIGPIC][/SIGPIC]<br />
    Apparent Etch</a>
  • Duncan CDuncan C Posts: 9,114Tutorial Authors, Registered Users @ @ @ @ @ @ @
    ansonl said:

    Duncan C said:


    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

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

    I'm available for one-on-one help at CodeMentor
  • jlptn1jlptn1 Posts: 60Registered Users
    Duncan C said:

    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.

  • jmbappsjmbapps Sindarin EreborPosts: 2New Users Noob
    Brilliant! Worked perfectly!

    JmB
  • butterflybeckybutterflybecky ChinaPosts: 3New Users Noob
    It's difficult work for me !
Sign In or Register to comment.