Geocoding Multiple Locations - Knowing When “All”

2019-05-20 23:21发布

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.

2条回答
淡お忘
2楼-- · 2019-05-20 23:38

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)

查看更多
Ridiculous、
3楼-- · 2019-05-20 23:40

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.

查看更多
登录 后发表回答