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];
}];
}
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.
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
})
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With