2

I am using the CoreLocation's geocoder to get the CLLocation coordinates for multiple map items. The geocoder calls a completion block on completion for each item.

How do I create a similar block functionality which is called when all of these containing asynchronous geocoder calls have been completed? (I could use a manual counter. But there must be a more elegant solution)

Here's my geocoding function so far. It loops through an array of location items and starts a new geocoding process for each.

-(void)geoCodeAllItems {
    for (EventItem* thisEvent in [[EventItemStore sharedStore] allItems]) {
        if (![thisEvent eventLocationCLLocation]){ //Only geocode if the item has no location data yet
            CLGeocoder *geocoder = [[CLGeocoder alloc]init];
            [geocoder geocodeAddressString:[thisEvent eventLocationGeoQuery] completionHandler:^(NSArray *placemarks, NSError *error) {
                if (error){
                     NSLog(@"\t Geo Code - Error - Failed to geocode";
                     return;
                 }
                 if (placemarks)
                 {
                     if ([placemarks count] > 1) NSLog(@"\t Geo Code - Warning - Multiple Placemarks (%i) returned - Picking the first one",[placemarks count]);

                     CLPlacemark* placemark = [[CLPlacemark alloc]initWithPlacemark:[placemarks objectAtIndex:0]];
                     CLLocationCoordinate2D placeCoord = [[placemark location]coordinate];
                     [thisEvent setEventLocationCLLocation:[[CLLocation alloc]initWithLatitude:placeCoord.latitude longitude:placeCoord.longitude]];

                     [[EventItemStore sharedStore] saveItems];
                 } else {
                     NSLog(@"\t Geo Code - Error - No Placemarks decoded");
                 }
             }];
            geocoder = nil;
        } 
    } 
}

This basically works however due to the asynchronous fashion I don't know when all geocoding activity has finally ended.

My feeling is, I either have to create an block for this or use Grand Central Dispatch but I am not really sure. I appreciate any help on this to find the right approach.

Bernd
  • 11,133
  • 11
  • 65
  • 98

2 Answers2

4

You can use a GCD dispatch group to do this. Also, I think you can make multiple requests with a single CLGeocoder.

Since we might not need to create the group and the geocoder at all, we'll create them lazily:

-(void)geocodeAllItems {
    dispatch_group_t group = NULL;
    CLGeocoder *geocoder = nil;

We loop over the items, skipping the ones that have already been geocoded:

    for (EventItem *item in [[EventItemStore sharedStore] allItems]) {
        if (item.eventLocationCLLocation)
            continue;

Now that we've found one that needs geocoding, we create the geocoder and the dispatch group if we need to:

        if (!geocoder) {
            geocoder = [[CLGeocoder alloc] init];
            group = dispatch_group_create();
        }

We'll use a helper method to geocode just this item:

        [self geocodeItem:item withGeocoder:geocoder dispatchGroup:group];
    }

Now that we've gone through all the items, we'll check whether we geocoded any:

    if (group) {

If we geocoded any, then there are blocks in the dispatch group. We'll ask the group to execute a notification block when it becomes empty:

        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"note: all geocoding requests have completed");
        });

Finally, we need to release the group to balance the +1 retain count returned by dispatch_group_create:

        dispatch_release(group);
    }
}

Here's the helper method that geocodes just one item:

- (void)geocodeItem:(EventItem *)item withGeocoder:(CLGeocoder *)geocoder dispatchGroup:(dispatch_group_t)group {

We “enter” the group. This increments the group's membership counter atomically:

    dispatch_group_enter(group);

Then we can start the geocoding:

    [geocoder geocodeAddressString:item.eventLocationGeoQuery completionHandler:^(NSArray *placemarks, NSError *error) {
        if (error) {
            NSLog(@"error: geocoding failed for item %@: %@", item, error);
        } else {

            if (placemarks.count == 0) {
                NSLog(@"error: geocoding found no placemarks for item %@", item);
            } else {
                if (placemarks.count > 1) {
                    NSLog(@"warning: geocoding found %u placemarks for item %@: using the first", item, placemarks.count);
                }
                CLPlacemark* placemark = placemarks[0];
                thisEvent.eventLocationCLLocation = placemarks[0].location;
                [[EventItemStore sharedStore] saveItems];
            }
        }

In the geocoding completion block, after all the work is done, we “leave” the group, which decrements its membership count:

        dispatch_group_leave(group);
    }];
}

When the membership count goes to zero, the group will execute the notification block we added at the end of geocodeAllItems.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Wow. What a great explanation and solution. This helps a lot and makes sense to me. However it doesn't quite work yet. The geocoder is only called one time and the completion is never reached. Note: I had to remove the "dispatch_release(group);" due to having ARC being turned on. – Bernd Dec 21 '12 at 16:18
  • 1
    I just noticed that the early return on error in the completion block would prevent the block from calling `dispatch_group_leave`. I have changed the code to always call `dispatch_group_leave`. – rob mayoff Dec 21 '12 at 20:09
  • I marked your answer as "accepted" because I appreciate the effort a lot. Thank you. However I am still struggling with getting the completion call in the right order. Therefore I created a new question with my updated code using the NSBlockOperationQueue. http://stackoverflow.com/questions/14126350/nsoperationqueue-getting-completion-call-too-early – Bernd Jan 02 '13 at 17:26
1

Take a look at NSBlockOperation used with NSOperationQueue

create an NSBlockOperation and then add all of the separate tasks as execution blocks addExecutionBlock: . Set your completion block setCompletionBlock: which is in the NSOperation super class and it will get called when all of the tasks are finished. They will by default not be run on the main thread so if you want your completion block to be executed on the main thread you will have to explicitly tell it to do so. Add the NSBlockOperation to a NSOperationQueue addOperation:(NSOperation *)operation

Reference to the Concurrency Guide page on Operation Queues

Recommend: WWDC 2012 video session 225 (skip to 16:55)

Samuel
  • 401
  • 2
  • 10
  • Thanks for this tip. Using the NSOperationQueue seems to be a good approach to me to solve this. However I still have some issues getting the completion messages into the right order. I created a new question including my updated code here: http://stackoverflow.com/questions/14126350/nsoperationqueue-getting-completion-call-too-early – Bernd Jan 02 '13 at 17:28
  • This answer is good on your other question: http://stackoverflow.com/a/14126568/667874 – Samuel Jan 02 '13 at 17:56
  • The issue here is that Bernd has operations, themselves, have asynchronously executed completion handlers. My solution was to have one operation for the geocode request, another for the completion handler, and then make each request contingent on the prior request's completion handler. See http://stackoverflow.com/questions/14195706/multiple-locations-in-google-map-mkmapitem/14198584#14198584 – Rob Jan 07 '13 at 15:16