Search code examples
ios5objective-c-blocks

Objective-C Blocks, Variables and CLGeocoder and/or CLPlacemark


I'm new to Objective-C and my C/C++ skills are quite rusty. What better time to learn iOS development(!)

I'm trying to reverse geolocate a position using the CLGeocoder class in iOS. I can successfully get the data I'm interested in (street address) inside the block/callback, however when I try to use that data to populate my variable (outside of the block) the data isn't there. It's as if the object in the block disappears before the MapView object calls it. I'm using __block which as I understand it, should allow the variable to persist outside the block, but it seems not to.

Here's the code in question:

- (void) foundLocation:(CLLocation *)loc
{
    CLLocationCoordinate2D coord = [loc coordinate];

    // Get our city and state from a reversegeocode lookup and put them in the subtitle field (nowString).
    // reversegeocode puts that information in a CLPlacemark object

    // First, create the CLGeocoder object that will get us the info
    CLGeocoder *geocoder = [[CLGeocoder alloc]init];

    // Next create a CLPlacemark object that we can store what reverseGeocodeLocation will give us containing the location data
    __block CLPlacemark *placemark = [[CLPlacemark alloc]init];

    __block NSString *sPlacemark = [[NSString alloc]init];

    // This next bit is where things go awry
    [geocoder reverseGeocodeLocation:loc completionHandler:
     ^(NSArray *placemarks, NSError *error) {
         if ([placemarks count] > 0)
         {
             placemark = [placemarks objectAtIndex:0];// this works!! 
             sPlacemark = [placemark thoroughfare]; // as does this! I can see the street address in the variable in the debugger.
         }
     }];

    MapPoint *mp = [[MapPoint alloc] initWithCoordinate:coord
                                                  title:[locationTitleField text] 
                                               subtitle:sPlacemark];

    // add it to the map view
    [worldView addAnnotation:mp];

    // MKMapView retains its annotations, we can release
    [mp release];

    // zoom region to this location
    MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coord, 250, 250);

    [worldView setRegion:region
                animated:YES];

    [locationTitleField setText:@""];
    [activityIndicator stopAnimating];
    [locationTitleField setHidden:NO];
    [locationManager stopUpdatingLocation];
}

I haven't completely wrapped my head around 'blocks' so it's likely that's where the problem is, but I can't put my finger on exactly what.


Solution

  • So what you have is a timing issue. The call to reverseGeocodeLocation:completionHandler: is asynchronous. The call itself will return immediately before the actual reverse geolocation happens.

    So immediately as the call returns, your method continues, and you're creating an annotation with a placemark that doesn't yet exist.

    Then at some point later, your reverse geolocation is finished (remember it requires a network call to a service and all of that). And then after that, your block is fired with the new incoming data.

    So by the time your block actually runs, these two local __block variables you created are long gone. Why? Because those variables placemark and sPlacemark are local (automatic) variables, local to this method. They come into existence within the method, and then go away when the method is finished. And again, this happens before your block even gets to run. (It turns out the block will write into a copy of each variable later, but that doesn't really matter, because this method is finished by then and you've already tried to read them too early.)

    If you put an NSLog message at the end of this method, and another one in your block, you'll see the sequence in which they fire. The order will be the opposite of what you're probably thinking. The one at the end of the method will fire first, and then the one inside the block.

    Think of this like a restaurant. The waiter goes back to the kitchen and places an order. Now he doesn't sit there at the kitchen and wait for the food to be cooked, because that takes time. So he leaves the order and continues his work until such time as the order is ready. Now imagine he leaves the order, and then immediately checks the counter. He'll be quite disappointed to see that the food isn't there yet. And that's exactly what you're doing when you immediately try to read the placemark variable before the cooks have had even a second to cook the order.

    So what's the answer? You could create another method that creates the annotation and places it on the map. That method should take the placemark as a parameter, and then you can call that method from within the block (which again is after you actually have the placemark.) Think of this as all the work the waiter would like to do after the order is ready, like take the order to the customer.

    There are other ways to do this. But for one single placemark as you show here, that's a simple way to handle this. Obviously, you should also add error handling in case you can't find the placemark, or the service is not available, etc. If the lookup fails, the placemark will be nil and you can check the error for more information.

    Hope that helps.