views:

2505

answers:

3

I have a UITableViewController, which is RootViewController. It's XIB only has a table view. I've add a UIActivityIndicator through IB and created an IBOutlet. I've programmatically added a toolbar with a delete button to the RootViewController. When the user clicks the toolbar's delete button, I want to display the indicator, centered on the screen. The method below deletes data from a database, which happens very fast. I want to let the user know something is happening. If I run the code below without sleep(), it happens to fast and I don't see the indicator. But some for some reason, I'm not seeing the indicator anyways. I'm guessing sleep() blocks repainting? If I take out sleep() and the last two lines, I see the indicator but of course it never goes away. It also displays in the top left of the window.

How do I get the code below to work so that the indicator displays for 1/2 - 1 second at least?

How do I align the indicator in the middle of the window?

[self.navigationController.view addSubview:activityIndicator];
[activityIndicator startAnimating];
[activityIndicator setNeedsDisplay];

//do something which might happen really fast

sleep(1); //create illusion
[activityIndicator stopAnimating];
[activityIndicator removeFromSuperview];
A: 

The problem is that while you're doing your thing, the main run loop isn't running. In order for the activity indicator to display, the main run loop needs to be running so it can update its animation. In order to get the intended behavior, you'll need to do your database operation asynchronously in a separate thread.

Adam Rosenfield
Does the API allow for multithreading?
4thSpace
Wow. I thought something like a "indicator to show activity" *WOULD* be designed to "start"... do my long task... and then "end".But why was it designed NOT to work like that at all. Isn't "show activity" the *WHOLE* point of this object????Ugh.
Bonnie
+4  A: 

The spinner will not start spinning until you've processed the event and returned to the run loop. The way around this is to ask the run loop to finish things up for you.

Here's an example of what I mean:

- (IBAction)deleteAction:(id)sender {
    [self.navigationController.view addSubview:activityIndicator];
    [activityIndicator startAnimating];

    // Spinner won't start spinning until we finish processing this event, so
    // we're just going to schedule the rest of what we need to do.

    // doDelete: will run when the main thread gets its next event.
    [self performSelectorOnMainThread:@selector(doDelete:)
                           withObject:record
                        waitUntilDone:NO];

    // removeSpinner: will run in at least one second, but will wait if
    // another event (like the doDelete: one) is in the middle of running.
    [self performSelector:@selector(removeSpinner:)
               withObject:activityIndicator
               afterDelay:1.0];
}
- (void)doDelete:(id)record {
    [record delete];   // or whatever it is you need to do
}
- (void)removeSpinner:(UIActivityIndicator*)activityIndicator {
    [activityIndicator stopAnimating];
    [activityIndicator removeFromSuperview];
}

Note: there's nothing in this code that is guaranteed to stop it from processing other touches during the "sleep" period, so make sure you lock out other events somehow.

Brent Royal-Gordon
Awesome! Thanks. Do you know if there is a way to make the indicator appear in the middle of the window? It appears nested in the upper left corner.
4thSpace
activityIndicator.center = CGPointMake(self.navigationController.view.frame.size.width/2, self.navigationController.view.frame.size.height/2);
Brent Royal-Gordon
Thanks again Brent. Great stuff.
4thSpace
As Adam mentions below, the spinner won't show unless the work (in this case the Delete operation) is done in a background thread. I've posted code in my own reply that I got to work based on Brent's initial code. I modified it to call my function that does the work in a background thread, and then to use the main thread to remove the spinner when done.
Jeremy Mullin
@Brent `
Dave DeLong
+1  A: 

As Adam mentioned, the spinner won't show unless the work (in this case the Delete operation) is done in a background thread. This is the code I got to work based on Brent's initial code. I modified it to call my function that does the work in a background thread, and then to use the main thread to remove the spinner when done.

- (void)removeSpinner:(id)record {
    [activityIndicator stopAnimating];
    [activityIndicator removeFromSuperview];

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Thank You" message:@"Thanks for your feedback!"
                                                                                                 delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [alert show];
    [alert release];

    // return to feedings tab
  tabBarController.selectedIndex = 0;
}


- (void)doFeedback:(id)record {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    int j;
    for (int i = 0; i < 1000000000; i++) {
        j = j + i;
    }
    [self performSelectorOnMainThread:@selector(removeSpinner:) withObject:nil waitUntilDone:NO];

    [pool release];
}


- (IBAction)handleSend {        
    [self.view addSubview:activityIndicator];
    [activityIndicator startAnimating];

    // Spinner won't start spinning until we finish processing this event, so
    // we're just going to schedule the rest of what we need to do. 
    // doFeedback: will run when the main thread gets its next event.   
    [self performSelectorInBackground:@selector(doFeedback:) withObject:nil];
}
Jeremy Mullin