How to search MKMapView with UISearchBar?

38,611

Solution 1

Ok, to answer my own question:

As was mentioned before, the best thing to do is to use the Google Maps API, it supports a lot of formats but for several reasons I chose to go with JSON.

So here are the steps to perform a JSON query to Google Maps and obtain the coordinate of the query. Note that not all the correct validations are done, this is only a Proof of concept.

1) Download a JSON framework/library for the iPhone, there are several, I chose to go with this one, it's very good and seems an active project, plus several comercial applications seem to be using it. So add it to your project ( instructions here ).

2) To query Google Maps for an address we need to build a request URL like this: http://maps.google.com/maps/geo?q=Paris+France

This url, will return a JSON object for the query "Paris+France".

3) Code:

//Method to handle the UISearchBar "Search", 
- (void) searchBarSearchButtonClicked:(UISearchBar *)theSearchBar 
{
    //Perform the JSON query.
    [self searchCoordinatesForAddress:[searchBar text]];

    //Hide the keyboard.
    [searchBar resignFirstResponder];
}

After we handle the UISearchBar search, we must make the request to Google Maps:

- (void) searchCoordinatesForAddress:(NSString *)inAddress
{
    //Build the string to Query Google Maps.
    NSMutableString *urlString = [NSMutableString stringWithFormat:@"http://maps.google.com/maps/geo?q=%@?output=json",inAddress];

    //Replace Spaces with a '+' character.
    [urlString setString:[urlString stringByReplacingOccurrencesOfString:@" " withString:@"+"]];

    //Create NSURL string from a formate URL string.
    NSURL *url = [NSURL URLWithString:urlString];

    //Setup and start an async download.
    //Note that we should test for reachability!.
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];

    [connection release];
    [request release];
}

We must of course then handle the response of the GoogleMaps server ( Note: a lot of validations missing)

//It's called when the results of [[NSURLConnection alloc] initWithRequest:request delegate:self] come back.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 
{   
    //The string received from google's servers
    NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

    //JSON Framework magic to obtain a dictionary from the jsonString.
    NSDictionary *results = [jsonString JSONValue];

    //Now we need to obtain our coordinates
    NSArray *placemark  = [results objectForKey:@"Placemark"];
    NSArray *coordinates = [[placemark objectAtIndex:0] valueForKeyPath:@"Point.coordinates"];

    //I put my coordinates in my array.
    double longitude = [[coordinates objectAtIndex:0] doubleValue];
    double latitude = [[coordinates objectAtIndex:1] doubleValue];

    //Debug.
    //NSLog(@"Latitude - Longitude: %f %f", latitude, longitude);

    //I zoom my map to the area in question.
    [self zoomMapAndCenterAtLatitude:latitude andLongitude:longitude];

    [jsonString release];
}

Finally the function to zoom my map, which should by now be a trivial thing.

- (void) zoomMapAndCenterAtLatitude:(double) latitude andLongitude:(double) longitude
{
    MKCoordinateRegion region;
    region.center.latitude  = latitude;
    region.center.longitude = longitude;

    //Set Zoom level using Span
    MKCoordinateSpan span;
    span.latitudeDelta  = .005;
    span.longitudeDelta = .005;
    region.span = span;

    //Move the map and zoom
    [mapView setRegion:region animated:YES];
}

Hope this helps someone because the JSON part was a real pain to figure out, the library is not very well documented in my opinion, still it's very good.

EDIT:

Modified one method name to "searchCoordinatesForAddress:" because of @Leo question. I have to say that this method is good as a proof of concept but if you plan to download big JSON files , you will have to append to a NSMutableData object to hold all the query to the google server. ( remember that HTTP queries come by pieces . )

Solution 2

This maybe the easiest method. It uses apple servers for geocoding. Sometimes the apple servers provide better response than google. And soon (in IOS 6.1) the google maps will be completely out of IOS. So it is good if the app stays inside the apples provided features.

-(void)searchBarSearchButtonClicked:(UISearchBar *)theSearchBar
{
    [theSearchBar resignFirstResponder];
    CLGeocoder *geocoder = [[CLGeocoder alloc] init];
    [geocoder geocodeAddressString:theSearchBar.text completionHandler:^(NSArray *placemarks, NSError *error) {
        //Error checking

        CLPlacemark *placemark = [placemarks objectAtIndex:0];
        MKCoordinateRegion region;
        region.center.latitude = placemark.region.center.latitude;
        region.center.longitude = placemark.region.center.longitude;
        MKCoordinateSpan span;
        double radius = placemark.region.radius / 1000; // convert to km

        NSLog(@"[searchBarSearchButtonClicked] Radius is %f", radius);
        span.latitudeDelta = radius / 112.0;

        region.span = span;

        [theMapView setRegion:region animated:YES];
    }];
}

Solution 3

If anyone else is having the same issue, heres the link: https://github.com/stig/json-framework/ scroll down to Project renamed to SBJson

Also, here is the code for getting all the data before your app uses it. Note the delegate method 'did receive data' as it appends the mutable data object with the downloaded data.

I JUST USED MR GANDOS searchCoodinatesMETHOD AS IT IS AS IT WORKS WELL

- (void) searchCoordinatesForAddress:(NSString *)inAddress
{
    //Build the string to Query Google Maps.
    NSMutableString *urlString = [NSMutableString stringWithFormat:@"http://maps.googleapis.com/maps/api/geocode/json?address=%@&sensor=false",inAddress];

    //Replace Spaces with a '+' character.
    [urlString setString:[urlString stringByReplacingOccurrencesOfString:@" " withString:@"+"]];

    //Create NSURL string from a formate URL string.
    NSURL *url = [NSURL URLWithString:urlString];

    //Setup and start an async download.
    //Note that we should test for reachability!.
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];

    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];

    [connection release];
    [request release];
}

// STEP ONE // THIS ONE IS IMPORTANT AS IT CREATES THE MUTABLE DATA OBJECT AS SOON AS A RESPONSE IS RECEIVED

-(void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
{
    if (receivedGeoData) 
    {
        [receivedGeoData release];
        receivedGeoData = nil;
        receivedGeoData = [[NSMutableData alloc] init];
    }
    else
    {
        receivedGeoData = [[NSMutableData alloc] init];
    }

}

/// STEP TWO // THIS ONE IS IMPORTANT AS IT APPENDS THE DATA OBJECT WITH THE DATA

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 
{   
    [receivedGeoData appendData:data]; 
}

// STEP THREE...... // NOW THAT YOU HAVE ALL THE DATA MAKE USE OF IT

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSString *jsonResult = [[NSString alloc] initWithData:receivedGeoData encoding:NSUTF8StringEncoding];
    NSError *theError = NULL;
    dictionary = [NSMutableDictionary dictionaryWithJSONString:jsonResult error:&theError];

    NSLog(@"%@",dictionary);

    int numberOfSites = [[dictionary objectForKey:@"results"] count];
    NSLog(@"count is %d ",numberOfSites);      
}

-(void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
{
    // Handle the error properly
}

Solution 4

This link helps you if you search a region.

NSMutableString *urlString = [NSMutableString stringWithFormat:@"http://maps.google.com/maps/geo?q=%@?output=json",inAddress];

If you want to search a street this is the corect link

NSMutableString *urlString = [NSMutableString stringWithFormat:@"http://maps.google.com/maps/geo?q=%@&output=json",inAddress];

Notice that the 2nd ? should be &.

Solution 5

Swift version, adapted for iOS 9:

let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in

    if let center = (placemarks?.first?.region as? CLCircularRegion)?.center {

        let region = MKCoordinateRegion(center: center, span: MKCoordinateSpanMake(0.02, 0.02))
        self.mapView.setRegion(region, animated: true)
    }
}

based on user1466453's answer.

Share:
38,611
Goles
Author by

Goles

Software Craftsman. Extremely interested in Web Technologies and Mobile Game Development. Using the sweet Obj-C, the evil C++ and the elegant Lua. Have been using lot's of Ruby spices in the mix lately.

Updated on July 09, 2022

Comments

  • Goles
    Goles almost 2 years

    I have an application that needs to have a similar search feature like the Apple "Maps" application (included with iPhone, iPod Touch and iPad).

    The feature in question should not be a hard thing to do, but I'm really clueless about how to input a Street Address in the search bar, and then obtaining coordinates for that address or something that can help me to actually move the map and center in that place.

    I mean, what do I have to query, does Apple provide an "address searching API method" ? or I need to use the google maps API directly ?

    I would love to hear how should it be done.

  • JakubM
    JakubM over 13 years
    Hi, but you don't solve a situation when there's more than one result on given address if I am reading your code right.
  • Goles
    Goles over 13 years
    Yeah, it shouldn't be hard to solve given this solution though.
  • seapy
    seapy about 13 years
    google geocoding api version is updated. code.google.com/apis/maps/documentation/geocoding
  • JakubKnejzlik
    JakubKnejzlik almost 12 years
    May I suggest using stringByAddingPercentEscapesUsingEncoding: method instead of replacing white spaces by "+"? :)
  • Goles
    Goles almost 12 years
    Yeah, that would be better I guess, did this a while ago. :)
  • NSTJ
    NSTJ over 11 years
    Beware the CLGeocoder is still in its infancy, and at time of writing seems not to return multiple placemark values. All things remaining equal though it looks to eclipse the Google Maps API option in the future
  • Raptor
    Raptor over 11 years
    one recommendation: use http://maps.google.com/maps/geo?q=%@&output=csv ( output in CSV mode ) to avoid parsing JSON. (no need to rely on JSON Framework)
  • Goles
    Goles over 11 years
    Well, technically for iOS 5+ JSON parsing is part of Cocoa-Touch :) so it's not that terrible either.
  • pmk
    pmk about 11 years
    it took me a while to spot the difference between this two string, but the "&" instead of "?" was extremly helpfull, thanks for that!
  • strongmayer
    strongmayer about 11 years
    This solution works fine on iphone 6.0, but I had problems with the setRegion method on iphone 5.0 and 5.1. I received the following exception:'NSInvalidArgumentException', reason: 'Invalid Region <center:+40.77026350, -73.97918700 span:+0.12243735, -1.99343872>'. The solution for this it to add the following line, after the region properties are set: region = [theMapView regionThatFits:region];
  • n13
    n13 over 10 years
    FWIW place mark may not have a region set, so the code above can crash. Just use another radius - say 500 meters - if placemark.region is nil. Other than that yes this is the way to go.
  • Rikki Gibson
    Rikki Gibson over 9 years
    This answer uses some deprecated properties that aren't allowed to be accessed in Swift. Anyone have the up-to-date approach?