5

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];
    }];
}
Sam Spencer
  • 8,492
  • 12
  • 76
  • 133
Grey Code
  • 326
  • 5
  • 19

2 Answers2

11

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • @GreyCode I realize that the subtleties of `NSOperationQueue` and the dependencies of `NSOperation` objects may get a little confusing, so I've updated my answer with a more traditional approach, that doesn't use `NSOperationQueue`. If there's something in particular you need explained, please let me know. (I'm not sure what your question is.) – Rob Jan 08 '13 at 12:57
  • @Rob-Hi Rob..i've doubt about NSArray..can u please join me.my mailId is "greycode.ios@gmail.com"..Like to Share and learn more from u Mr.Rob..Thanks – Grey Code Jan 08 '13 at 13:57
  • @Rob-http://stackoverflow.com/questions/14212664/nsarray-not-detected-in-mkmapitem..hi Rob..can u understand my question?? – Grey Code Jan 09 '13 at 07:28
  • @GreyCode Yes. I think you're confused about `[friend.location objectForKey:@"name"]`, which is `NSString`, not `NSArray`. But I show you in my comment to that other question, how to build an array from those strings. – Rob Jan 09 '13 at 08:46
  • @Rob-while using this it's hanging and displays error..also i've posted another question..http://stackoverflow.com/questions/14235717/passing-user-location-to-mkmapitem – Grey Code Jan 09 '13 at 13:00
  • I keep getting *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MKMapItem length]: unrecognized selector sent to instance 0x7f93f5133640' – Jordan Brown Aug 21 '14 at 23:34
  • Thanks @Rob I attempted to catch it with an exception breakpoint but was unsuccessful :(. I just posted my own question. http://stackoverflow.com/questions/25438928/gecode-mklocalsearchresults-response – Jordan Brown Aug 22 '14 at 03:06
  • BTW, I've simplified my answer here, as the previous code sample was way too convoluted. – Rob Aug 22 '14 at 05:43
1

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
      })

}
Rakesh Gujari
  • 899
  • 1
  • 8
  • 12
  • 2
    This approach does not solve the concurrency issue, because you don't wait for the first request to complete before sending the second request. – Felix Jan 17 '19 at 11:15