Forward Geocoding
It is a common scenario that every developer has run into.
Locate places near me based upon a particular address and/or zip code.
In order to get the correct results the address needs to be converted to a lat/long coordinate. With iOS5 Apple has provided us with CLGeoCoder class which has convience methods for reverse/forward geocoding. Unfortunately, there isn’t really any built-in functionality for iOS4. Most current apps still have to support iOS4 so what are developers doing to fill the gap? Write their implementations for multiple OS support. Below is what I use.
** Note: I did use Sergio Martínez-Losa Del Rincón article as a starting point for the iOS4 support.
Techniques
- Google’s Map API
- CLGeoCoder
- Objective-C blocks for callback
- Grand Central Dispatch
Implementation
iOS5
I tackled the easiest solution first. Utilizing iOS5’s CLGeoCoding
geocodeAddressString:completionHandler:
. However, when I first use
the method I kept getting back a location of {0.0,0.0} in my log. The
reason being that I was neglecting the fact that the method executes
asynchronously and doesn’t return its value. Hmmm….
Not too much of a hurdle thanks to the implementation of block handling. By adding a completionHandler to my own method I was able to provide a callback with the appropriate coordinate that I need after the request had finished.
iOS4
Duplicating the ease of the iOS5 functionality was a bit more involved. I created a category method on NSString (see the above referenced article for code origin. I did make updates to it.) for making a call to the Google Maps API and return their forward coding result. The method itself isn’t asynchronous because I wanted to leave the decision of whether or not to call the method async or sync up to the developer. To provide asynchrous support was easy just by utiliizing GCD.
Decision Time
Deciding which code block should the calling method execute was the final task to finish. Once again this was extremely simple.
if (NSClassFromString(@“CLGeocoder”)) { // ios5 } else { // everything else. in this case we are assuming there isn’t support for iOS3.x }
Code
// Block param typedef | |
typedef void (^ForwardGeoCompletionBlock)(CLLocationCoordinate2D coords); | |
// Fetch Records Method | |
- (void)fetchForwardGeocodeAddress:(NSString *)address withCompletionHanlder:(ForwardGeoCompletionBlock)completion { | |
if (NSClassFromString(@"CLGeocoder")) { | |
CLGeocoder *geocoder = [[CLGeocoder alloc] init]; | |
CLGeocodeCompletionHandler completionHandler = ^(NSArray *placemarks, NSError *error) { | |
if (error) { | |
NSLog(@"error finding placemarks: %@", [error localizedDescription]); | |
} | |
if (placemarks) { | |
[placemarks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { | |
CLPlacemark *placemark = (CLPlacemark *)obj; | |
NSLog(@"PLACEMARK: %@", placemark); | |
if ([placemark.country isEqualToString:@"United States"]) { | |
NSLog(@"********found coords for zip: %f %f", placemark.location.coordinate.latitude,placemark.location.coordinate.longitude); | |
if (completion) { | |
completion(placemark.location.coordinate); | |
} | |
*stop = YES; | |
} | |
}]; | |
} | |
}; | |
[geocoder geocodeAddressString:address completionHandler:completionHandler]; | |
} else { | |
/** | |
* Since the request to grab the geocoded address is using NSString's initWithContentsOfURL | |
* we are going to make use of GCD and grab that async. I didn't put this in the | |
* method itself because there could be an instance where you would want to make a | |
* synchronous call. Better to have flexibility. | |
* | |
* Possible improvement could be to write another method that does the async | |
* loading for you. However, if you did it that way how would you be notified | |
* when the results returned. Possibly KVO? | |
*/ | |
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); | |
dispatch_async(queue, ^{ | |
CLLocation *location = [address newGeocodeAddress]; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
if (completion) { | |
completion(location.coordinate); | |
} | |
}); | |
}); | |
} | |
} | |
// NSString Category (JSONKit is dependency for project since there isn't a native parser in iOS4 | |
@interface NSString (Additions) | |
- (CLLocation *)newGeocodeAddress; | |
@end | |
#import "NSString+Additions.h" | |
@implementation NSString (Additions) | |
- (CLLocation *)newGeocodeAddress { | |
__block CLLocation *location = nil; | |
NSString *gUrl = [NSString stringWithFormat:@"http://maps.googleapis.com/maps/api/geocode/json?address=%@&sensor=false", self]; | |
gUrl = [gUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; | |
NSString *infoData = [[[NSString alloc] initWithContentsOfURL:[NSURL URLWithString:gUrl] | |
encoding:NSUTF8StringEncoding | |
error:nil] autorelease]; | |
if ((infoData == nil) || ([infoData isEqualToString:@"[]"])) { | |
return location; | |
} else { | |
NSDictionary *jsonObject = [infoData objectFromJSONString]; | |
if (jsonObject == nil) { | |
return nil; | |
} | |
NSArray *result = [jsonObject objectForKey:@"results"]; | |
[result enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { | |
NSDictionary *value = [[obj objectForKey:@"geometry"] valueForKey:@"location"]; | |
location = [[CLLocation alloc] initWithLatitude:[[value valueForKey:@"lat"] doubleValue] | |
longitude:[[value valueForKey:@"lng"] doubleValue]]; | |
*stop = YES; | |
}]; | |
} | |
return location; | |
} | |
@end | |
// Request Execution | |
[[MyLocationManager sharedManager] fetchForwardGeocodeAddress:@"37415" withCompletionHanlder:^(CLLocationCoordinate2D coords) { | |
NSLog(@"found coords for zip: %f %f", coords.latitude, coords.longitude); | |
}]; |