Multiple Locations on Map (using MKMapItem and CLGeocoder)

4.2k views Asked by At

I'm trying to display multiple locations in MKMapItem. I am getting those locations from a CLGeocoder, unfortunately it only accepts one location. Even though I pass in an NSArray it just returns one location.

The following works fine with a single location, but not with multiple locations. How can I geocode multiple locations?

Class mapItemClass = [MKMapItem class];
if (mapItemClass && [mapItemClass respondsToSelector:@selector(openMapsWithItems:launchOptions:)]) {
    NSArray *addresses = @[@"Mumbai",@"Delhi","Banglore"];

    CLGeocoder *geocoder = [[CLGeocoder alloc] init];
    [geocoder geocodeAddressString:@[addresses] completionHandler:^(NSArray *placemarks, NSError *error) {
        CLPlacemark *geocodedPlacemark = [placemarks objectAtIndex:0];
        MKPlacemark *placemark = [[MKPlacemark alloc] initWithCoordinate:geocodedPlacemark.location.coordinate addressDictionary:geocodedPlacemark.addressDictionary];
        MKMapItem *mapItem = [[MKMapItem alloc] initWithPlacemark:placemark];
        [mapItem setName:geocodedPlacemark.name];

        [MKMapItem openMapsWithItems:@[mapItem] launchOptions:nil];
    }];
}
3

There are 3 answers

0
Rob On

For contemporary Swift readers, async-await greatly simplifies the process:

func mapItems(for strings: [String]) async throws -> [MKMapItem] {
    var mapItems: [MKMapItem] = []
    let geocoder = CLGeocoder()
    
    for string in strings {
        try await geocoder.geocodeAddressString(string)
            .map { MKPlacemark(placemark: $0) }
            .map { MKMapItem(placemark: $0) }
            .forEach { mapItems.append($0) }
    }
    
    return mapItems
}

Swift concurrency handles dependencies between asynchronous tasks far more elegantly than the old NSOperation approach we had to do in Objective-C.


Or, if searching on a map, perhaps MKLocalSearch:

extension MKMapView {
    func mapItems(for strings: [String]) async throws -> [MKMapItem] {
        var results: [MKMapItem] = []
        
        for string in strings {
            let request = MKLocalSearch.Request()
            request.naturalLanguageQuery = string
            request.region = region
            let mapItems = try await MKLocalSearch(request: request).start().mapItems
            results.append(contentsOf: mapItems)
        }
        
        return results
    }
}

Needless to say, as noted above, the whole idea of multiple geocoding requests is an anti-pattern, explicitly discouraged in the CLGeocoder documentation.

1
Rakesh Gujari On

For Guys looking for Swift Solution:

func getCoordinate( addressString : String, completionHandler: @escaping(CLLocationCoordinate2D, NSError?) -> Void ){
            let geocoder = CLGeocoder()
            geocoder.geocodeAddressString(addressString) { (placemarks, error) in
                if error == nil {
                    if let placemark = placemarks?[0] {
                        let location = placemark.location!
                        completionHandler(location.coordinate, nil)
                        return
                    }
                }

                completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
            }
        }

Thanks to Apple. Official doc link

You can use the snippet in the following way for multiple addresses:

let address = ["India","Nepal"]

for i in 0..<address.count {
 getCoordinate(addressString: address[i], completionHandler: { (location, error) in
                //Do your stuff here. For e.g. Adding annotation or storing location
      })

}
8
Rob On

In answer to your question, you are correct that you can only send one geocode request at one time. In fact, the CLGeocoder Class Reference says that our apps should "send at most one geocoding request for any one user action."

So, to do this, you must send separate requests. But these requests (which run asynchronously) should not be running concurrently. So, the question is how to make a series of asynchronous geocode requests run sequentially, one after another.

There are lots of different ways of tackling this, but one particularly elegant approach is to use a concurrent NSOperation subclass, which doesn't complete the operation (i.e. doesn't perform the isFinished KVN) until the asynchronous completion block of the geocode request is called. (For information about concurrent operations, see the Configuring Operations for Concurrent Execution section of the Operation Queue chapter of the Concurrency Programming Guide). Then just add those operations to a serial operation queue.

Another approach is to make this asynchronous geocode request behave in a synchronous manner, and then you can just add the requests to a serial queue and the requests will be performed sequentially rather than in parallel. You can achieve this through the use of semaphores, effectively instructing the dispatched task to not return until the geocode request complete. You could do it like so:

CLGeocoder *geocoder = [[CLGeocoder alloc]init];
NSMutableArray *mapItems = [NSMutableArray array];

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;   // make it a serial queue

NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
    [MKMapItem openMapsWithItems:mapItems launchOptions:nil];
}];

NSArray *addresses = @[@"Mumbai, India", @"Delhi, India", @"Bangalore, India"];

for (NSString *address in addresses) {
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

        [geocoder geocodeAddressString:address completionHandler:^(NSArray *placemarks, NSError *error) {
            if (error) {
                NSLog(@"%@", error);
            } else if ([placemarks count] > 0) {
                CLPlacemark *geocodedPlacemark = [placemarks objectAtIndex:0];
                MKPlacemark *placemark = [[MKPlacemark alloc] initWithCoordinate:geocodedPlacemark.location.coordinate
                                                               addressDictionary:geocodedPlacemark.addressDictionary];
                MKMapItem *mapItem = [[MKMapItem alloc] initWithPlacemark:placemark];
                [mapItem setName:geocodedPlacemark.name];

                [mapItems addObject:mapItem];
            }
            dispatch_semaphore_signal(semaphore);
        }];

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }];

    [completionOperation addDependency:operation];
    [queue addOperation:operation];
}

[[NSOperationQueue mainQueue] addOperation:completionOperation];

Alternatively, you could employ more traditional patterns, too. For example, you could write a method that performs a single geocode request, and in the completion block, initiates the next request, and repeat that process until all the requests are made.