Multiple Locations on Map (using MKMapItem and CLGeocoder)
Asked Answered
A

3

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];
    }];
}
Ambler answered 7/1, 2013 at 12:12 Comment(1)
hello..can any1 guide me..Thanks in advanceAmbler
G
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.

Gaitan answered 7/1, 2013 at 15:8 Comment(8)
@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.)Gaitan
@Rob-Hi Rob..i've doubt about NSArray..can u please join me.my mailId is "[email protected]"..Like to Share and learn more from u Mr.Rob..ThanksAmbler
@Rob-#14213164 Rob..can u understand my question??Ambler
@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.Gaitan
@Rob-while using this it's hanging and displays error..also i've posted another question..#14236217Ambler
I keep getting *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MKMapItem length]: unrecognized selector sent to instance 0x7f93f5133640'Baroja
Thanks @Gaitan I attempted to catch it with an exception breakpoint but was unsuccessful :(. I just posted my own question. #25439428Baroja
BTW, I've simplified my answer here, as the previous code sample was way too convoluted.Gaitan
S
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
      })

}
Spartan answered 20/12, 2017 at 15:52 Comment(1)
This approach does not solve the concurrency issue, because you don't wait for the first request to complete before sending the second request.Av
G
0

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.

Gaitan answered 12/11, 2023 at 18:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.