0

So when I click on a callout accessory in my mapView, nothing happens for several seconds because it is making a url request and parsing it, so I wanted to show the activity indicator so the user doesn't think it's frozen. Here's the code:

- (void)mapView:(MKMapView *)mv annotationView:(MKAnnotationView *)pin calloutAccessoryControlTapped:(UIControl *)control {
    // start activity indicator 
    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
    NSLog(@"tapped");

    ArtPiece *artPiece = (ArtPiece *)pin.annotation;

    //when annotation is tapped switches page to the art description page
    artDescription *artD = [[artDescription alloc] initWithNibName:@"artDescription" bundle:nil];
    artD.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
    artD.startingLocation = mapView.userLocation.location.coordinate;
    artD.selectedArtPiece = artPiece;
    NSLog(@"0");
    [self presentModalViewController:artD animated:YES];
    NSLog(@"1");

    [artD loadArt:artPiece];
    NSLog(@"2");
    // stop activity indicator 
    //[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

    [artD release];
}

Strangely (to me anyway, maybe I'm missing something obvious as I'm pretty inexperienced), the activity indicator does not show until after the method is done, and the modal view starts animating into view. I put the NSLogs in to see what was taking time. I had about a 2 second pause between "0" and "1" and another couple seconds between "1" and "2". Then the indicator finally showed, so I am sure it is waiting until the end of the method for some reason. Any ideas why?

Halle
  • 3,584
  • 1
  • 37
  • 53
Ryan
  • 570
  • 9
  • 25

3 Answers3

4

The change to the UI, displaying the activity indicator, does not take effect until control has returned to the application's main run loop. This does not occur until after your method has ended and the stack has unwound. You need to show the activity indicator, then dump the activity you are waiting for onto a background thread:

[self performSelectorInBackground:@selector(doThingINeedToWaitFor:)
                       withObject:anObject];

(Note that Apple recommends that you move away from using threads explicitly; performSelectorInBackground:withObject: is the simplest method to get some code run off the main thread. More complex options are available for other situations. See the Concurrency Programming Guide.)

The important gotcha is that UI updates still need to be handled on the main thread, so in that method, when the work is done, you need to call back to stop the activity indicator:

- (void) doThingINeedToWaitFor: (id)anObject {
    // Creating an autorelease pool should be the first thing
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    // Do your work...
    // ...
    // Update the UI back on the main thread
    [self performSelectorOnMainThread:@selector(allDoneWaiting:)
                           withObject:nil
                        waitUntilDone:YES];
    // Clear out the pool as the final action on the thread
    [pool drain];
}

In your callback method, you hide the activity indicator again and do any other post-processing that's necessary.

jscs
  • 63,694
  • 13
  • 151
  • 195
  • So would allDoneWaiting be another method that just did: [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; ? – Ryan Aug 19 '11 at 20:10
  • It might do other things dependent on your needs, but basically, yes. Any code that touches the GUI needs to be run on the main thread. – jscs Aug 19 '11 at 20:16
  • Ok thanks for the help, one more question: It's crashing and complaining about an autorelease pool, do I need to make one around what's being done in the doThingINeedToWaitFor method? – Ryan Aug 19 '11 at 20:24
  • Yes, new threads need their own autorelease pool set up in their main function. – jscs Aug 19 '11 at 20:28
  • Thanks, it all seems to be working now and I've checked yours as best answer, but... since all of that is going on in the background, the mapView with the callout is not being blocked, so the user can select another annotation and click its accessory button while the first one is still doing it's work. Is there a way to block the UI after the callout click in such a way that the activityindicator still updates as it should, but the user cannot still interact until it is done? – Ryan Aug 19 '11 at 20:53
  • That'll depend on specifics of your app. The simplest (and most unfriendly) thing to do would be throwing up a translucent gray layer to completely block the UI while you're doing the work. Ideally you'd prevent interaction in a more refined way. You could set a flag, and have another path in `mapView:annotationView:calloutAccessoryControlTapped:` to indicate somehow to the user that you can't accept any more input, or to cancel the previous tap, or whatever seems appropriate. – jscs Aug 19 '11 at 21:06
3

You cannot start and stop the activity indicator in the same function.

See the answer I provided for this question: How show activity-indicator when press button for upload next view or webview?

Edit for clarity:

- (void) someFunction
{
    [activityIndicator startAnimation];

    // do computations ....

    [activityIndicator stopAnimation];  
}

The above code will not work because you do not give the UI time to update when you include the activityIndicator in your currently running function. So what I and many others do is break it up into a separate thread like so:

- (void) yourMainFunction {
    activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];

    [NSThread detachNewThreadSelector:@selector(threadStartAnimating) toTarget:self withObject:nil];

    //Your computations

    [activityIndicator stopAnimating];

}

- (void) threadStartAnimating {
    [activityIndicator startAnimating];
}
Community
  • 1
  • 1
Karoly S
  • 3,180
  • 4
  • 35
  • 55
  • 1
    UI changes need to happen in the main thread. Activity indicator needs to startAnimating in `yourMainFunction`, a thread needs to be "detached" (GCD,NSOperation,NSThread,POSIX) to do the computations and then call back to the main thread to stopAnimating. – Joe Aug 19 '11 at 19:07
0

Something is slowing down your spinner. I would recommend doing your heavy lifting in background, using a thread. Try this:

-(void)myMethod{
    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
    [NSThread detachNewThreadSelector:@selector(startWorkingThread) toTarget:self withObject:nil];
}

-(void)startWorkingThread{
     //Heavy lifting
     [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}

I assume that you have commented the:

[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

For testing purposes...

Rui Peres
  • 25,741
  • 9
  • 87
  • 137