Chapter 5. Geocoding

If you were developing on a platform that didn’t support geocoding, you’d need to make use of one of the many web services that provide geocoding (see later in this chapter for one such service). However, with the arrival of iOS 5, Apple has provided native support for both forward and reverse geocoding.

Note

Although reverse geocoding was provided in the Map Kit framework by the now deprecated MKReverseGeocoder class, before the arrival of iOS 5, there was no forward geocoding capabilities offered natively by the SDK. With the introduction of the CLGeocoder class, part of the Core Location framework, both capabilities are now natively provided. The CLGeocoder class should be used for all new application development.

Reverse Geocoding

Warning

Both forward and reverse geocoding requests make a network connection back to web services hosted by Apple. The calls will fail without a network connection.

Reverse geocoding is the process of converting coordinates (latitude and longitude) into place name information. From iOS 5 you should use the CLGeocoder class to make reverse-geocoding requests by passing it a CLLocation object.

CLLocation *location =
    [[CLLocation alloc] initWithLatitude:37.323 longitude:-122.031];
CLGeocoder *geocoder = [[CLGeocoder alloc] init];

[geocoder reverseGeocodeLocation: location
    completionHandler: ^(NSArray *placemarks, NSError *error) {1

        for (CLPlacemark *placemark in placemarks) {

            NSLog(@"Placemark is %@", placemark);

        }
    }];
1

The completion handler block is called when the reverse geocoding request returns.

Using Alternative Reverse Geocoding Services

There are many web services that provide reverse geocoding. One of these is offered by the GeoNames.org site and it will return an XML or JSON document listing the nearest populated place using reverse geocoding. Requests to the service take this form if you want an XML document returned, or this form if you prefer a JSON document. There are several optional parameters: radius (in km), max (maximum number of rows returned), and style (SHORT, MEDIUM, LONG, and FULL).

Passing the longitude and latitude of Cupertino, CA, the JSON service would return the following JSON document:

{
   "geonames":[
      {
         "countryName":"United States",
         "adminCode1":"CA",
         "fclName":"city, village,...",
         "countryCode":"US",
         "lng":-122.0321823,
         "fcodeName":"populated place",
         "distance":"0.9749",
         "fcl":"P",
         "name":"Cupertino",
         "fcode":"PPL",
         "geonameId":5341145,
         "lat":37.3229978,
         "population":50934,
         "adminName1":"California"
      }
   ]
}

Forward Geocoding

Forward geocoding is the process of converting place names into coordinates (latitude and longitude). From iOS 5 you can use the CLGeocoder class to make forward-geocoding requests using either a dictionary of Address Book information (see Chapter 11) or an NSString. There is no designated format for string-based requests; delimiter characters are welcome, but not required, and the geocoding service treats the string as case-insensitive.

NSString *address = @"1 Infinite Loop, CA, USA";
CLGeocoder *geocoder = [[CLGeocoder alloc] init];

[geocoder geocodeAddressString:address
    completionHandler:^(NSArray* placemarks, NSError* error){ 1

        for (CLPlacemark *placemark in placemarks) {

            NSLog(@"Placemark is %@", placemark);

        }
    }];
1

The completion handler block is called when the reverse geocoding request returns.

Building an Example App

Let’s build a quick example application.

Open Xcode, choose Create a new Xcode project in the startup window, and then choose the Single View Application template from the iOS section of the New Project popup window. When prompted, name your new project GeoCoder. In the Company Identifier box, enter the root part of your Bundle Identifier used in your Provisioning Profile (see the iOS Provisioning Portal if you don’t know it); for me, this is uk.co.babilim. You should leave the Class Prefix box blank and ensure that the Device Family is set to iPhone, with the checkbox for ARC ticked. The boxes for storyboard and unit tests should not be ticked. Save your project, and then when the Xcode project window opens, add the Core Location framework.

Click the project icon at the top of the Project pane in Xcode. Click the main Target for the project, and then click the Build Phases tab. Finally, click the Link Binary with Libraries item to open up the list of linked frameworks, and click the + symbol to add a new framework. Select the CoreLocation.framework from the drop-down list and click the Add button to add the framework to your project.

Now that you’ve imported the Core Location framework, let’s build the user interface. Open up the ViewController.xib file and drag and drop five UILabel objects, three UITextField objects, and two UIButton objects into the view. Position them as in Figure 5-1.

The user interface

Figure 5-1. The user interface

Open the Assistant editor and right-click and drag the three UITextField objects into the View Controller code to create three IBOutlet properties. Call them latitude, longitude, and address, respectively. Then right-click and drag the two UIButton objects into the code and create an IBAction for each (see Figure 5-2). Call the methods reverseButton: and forwardButton:, respectively.

Connecting the outlets and actions to the view controller code

Figure 5-2. Connecting the outlets and actions to the view controller code

Next, right-click and drag each of the three UITextField objects to the File’s Owner icon at the top of the Dock and connect them to the delegate outlet (see Figure 5-3).

Make File’s Owner the delegate of all three UITextField objects

Figure 5-3. Make File’s Owner the delegate of all three UITextField objects

Save your changes and close the Assistant editor. Click the ViewController.h interface file to open it in the Standard editor.

The UITextFieldDelegate protocol offers a rich set of delegate methods. To use them, you must declare your class as implementing that delegate protocol. So, declare that your view controller implements the protocol, and additionally declare an instance variable to point to whichever of your three UITextField objects is actively being edited by the user.

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UITextFieldDelegate> {

    UITextField *activeTextField;1
}

@property (weak, nonatomic) IBOutlet UITextField *latitude;
@property (weak, nonatomic) IBOutlet UITextField *longitude;
@property (weak, nonatomic) IBOutlet UITextField *address;

- (IBAction)reverseButton:(id)sender;
- (IBAction)forwardButton:(id)sender;

@end
1

If your application has more than one text field in the view, it’s useful to keep track of which is currently the active field by using an instance variable.

Save your changes and open the corresponding ViewController.m implementation file. When the user taps the text field, the textFieldShouldBeginEditing: method is called in the delegate to ascertain whether the text field should enter edit mode and become the first responder.

- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
    activeTextField = textField;
    return YES;
}

When editing ends, the textFieldShouldReturn: method is called in the delegate. You can use this callback to resign as first responder, which will have the effect of making the keyboard disappear from your view.

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    activeTextField = nil;
   [textField resignFirstResponder];
    return YES;
}

Add both of these delegate callbacks to your code, and import the Core Location framework; we’re going to need it.

#import <CoreLocation/CoreLocation.h>

Move to the reverseButton: method, and add the following code:

- (IBAction)reverseButton:(id)sender {

    CLLocation *location = [[CLLocation alloc]
                             initWithLatitude:[self.latitude.text doubleValue]
                                    longitude:[self.longitude.text doubleValue]];

    NSLog(@"location = %@", location);
    CLGeocoder *geocoder = [[CLGeocoder alloc] init];

    [geocoder reverseGeocodeLocation: location
                   completionHandler: ^(NSArray *placemarks, NSError *error) {

                       NSLog(@"completed");
                       if ( error ) {
                           NSLog(@"error = %@", error );
                           dispatch_async(dispatch_get_main_queue(),^{

                               UIAlertView *alert = [[UIAlertView alloc]
                                     initWithTitle:@"Error"
                                     message:[self errorMessage:error.code]
                                     delegate:nil
                                     cancelButtonTitle:nil
                                     otherButtonTitles:@"OK", nil];
                               [alert show];
                               });
                       }
                       for (CLPlacemark *placemark in placemarks) {

                           NSLog(@"Placemark is %@", placemark);
                           UIAlertView *alert = [[UIAlertView alloc]
                               initWithTitle:@"Result"
                               message:[NSString stringWithFormat:@"%@",placemark]
                               delegate:nil
                               cancelButtonTitle:nil
                               otherButtonTitles:@"OK", nil];
                           [alert show];
                       }
                   }];
}

- (NSString *)errorMessage:(int) code {1

    NSString *message = nil;
    switch (code) {
        case kCLErrorLocationUnknown:
            message = @"Location Unknown";
            break;
        case kCLErrorDenied:
            message = @"Denied";
            break;
        case kCLErrorNetwork:
            message = @"Network Error";
            break;
        case kCLErrorGeocodeFoundNoResult:
            message = @"No Result Found";
            break;
        case kCLErrorGeocodeFoundPartialResult:
            message = @"Partial Result";
            break;
        case kCLErrorGeocodeCanceled:
            message = @"Cancelled";
            break;
        default:
            message = @"Unknown Error";
            break;
    }
    return message;

}
1

You’re using the errorMessage: convenience method to generate a human readable string from the returned error code in those cases where you have an error.

This takes the latitude and longitude text fields and passes them to the geocoder, which then makes a network call to Apple to resolve to the nearest placemark (or possibly multiple placemarks).

Save your changes and click the Run button in the Xcode toolbar to build and deploy your application into the iPhone Simulator (or onto your device).

Warning

The iPhone Simulator can sometimes have problems doing geocoding requests. If you have problems, try deploying your code onto your device and testing it there.

If all goes well, you should see something like Figure 5-4. Enter a latitude and longitude (in decimal degrees) and tap on the “Go” button, and you should get a result back. This should occur almost instantly.

Your user interface (left), an example of successful reverse geocoding (center), and failed reverse geocoding (right)

Figure 5-4. Your user interface (left), an example of successful reverse geocoding (center), and failed reverse geocoding (right)

Everything seems to be working, so let’s finish up and implement forward geocoding. There’s just one problem. Open up the application, either in the iPhone Simulator or on your device, and then tap on your third UITextField object. The keyboard will present itself, and cover the entry widget. That’s not optimal, so let’s fix that before proceeding.

Click the ViewController.xib file to open it in Interface Builder. For each of the three UITextField objects, open the Attributes Inspector in the Utility Panel and set the tag value. Set the latitude field to have a tag of 1, the longitude to have a tag of 2, and the address field to have a tag of 3 (see Figure 5-5).

Setting the tag value of the third UITextField to 3

Figure 5-5. Setting the tag value of the third UITextField to 3

You can use these tags in your code in order to tell the difference between the three text fields, and then move the main view appropriately so that the text field is no longer underneath the keyboard when it is presented. The obvious place to do this is from the text field delegate methods.

In the textFieldShouldBeginEditing: method, add the following code:

- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
    activeTextField = textField;

    CGAffineTransform transform;
    switch (textField.tag) {
        case 1:
            transform = CGAffineTransformMakeTranslation(0,0);
            break;
        case 2:
            transform = CGAffineTransformMakeTranslation(0,-50);
            break;
        case 3:
            transform = CGAffineTransformMakeTranslation(0,-120);
            break;
        default:
            transform = CGAffineTransformMakeTranslation(0,0);
            break;
    }
    [UIView beginAnimations:@"MoveAnimation" context:nil];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationDuration:0.3];
    self.view.transform = transform;
    [UIView commitAnimations];

    return YES;
}

You can see that you translate the position of the main view by different amounts depending on which of the three text fields is the active text field. Then, in the textFieldShouldReturn: method, move the main view back to its original position:

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    activeTextField = nil;

    CGAffineTransform transform = CGAffineTransformMakeTranslation(0,0);
    [UIView beginAnimations:@"MoveAnimation" context:nil];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationDuration:0.3];
    self.view.transform = transform;
    [UIView commitAnimations];

    [textField resignFirstResponder];
    return YES;
}

Save your changes and deploy the application into the iPhone Simulator. You should see the behavior you’re expecting, with the main view moving up and down depending on which of the text fields is selected (see Figure 5-6).

With no field selected (left), with the top field selected (center left), with the second field selected (center right), and with the final field selected (right)

Figure 5-6. With no field selected (left), with the top field selected (center left), with the second field selected (center right), and with the final field selected (right)

Now that you’ve fixed that fairly serious bug in your user interface, you can implement the forwardButton: method. You can reuse the errorMessage: convenience method from earlier; the code looks a lot like the previous example, so there’s not a lot to do here.

- (IBAction)forwardButton:(id)sender {

    NSString *place = self.address.text;
    CLGeocoder *geocoder = [[CLGeocoder alloc] init];

    [geocoder geocodeAddressString:place
                 completionHandler:^(NSArray* placemarks, NSError* error){

        NSLog(@"completed");
        if ( error ) {
            NSLog(@"error = %@", error );
            dispatch_async(dispatch_get_main_queue(),^{

                UIAlertView *alert = [[UIAlertView alloc]
                         initWithTitle:@"Error"
                               message:[self errorMessage:error.code]
                              delegate:nil
                     cancelButtonTitle:nil
                     otherButtonTitles:@"OK", nil];
                [alert show];

            });

        }
        for (CLPlacemark *placemark in placemarks) {

            NSLog(@"Placemark is %@", placemark);
            UIAlertView *alert = [[UIAlertView alloc]
                     initWithTitle:@"Result"
                           message:[NSString stringWithFormat:@"%@", placemark]
                          delegate:nil
                 cancelButtonTitle:nil
                 otherButtonTitles:@"OK", nil];
            [alert show];

        }
    }];

}

Save your changes and click the Run button to build and deploy your application. Enter an address in the address text field and hit the related Go button. If all goes well, you should see an alert view with the corresponding latitude and longitude (see Figure 5-7).

Successfully forward geocoding an address to a latitude and longitude

Figure 5-7. Successfully forward geocoding an address to a latitude and longitude

CLPlacemark Objects

Both forward and reverse geocoding requests return an array of CLPlacemark objects. While you’ve just dropped the placemark directly into your alert view, it’s a fairly rich source of data. It stores data including information such as the country, state, city, and street address associated with the specified latitude and longitude coordinate, although it can also include points of interest and geographically-related data nearby.

Adding CLPlacemarks to Maps

While a CLPlacemark can’t be immediately added to a map view, it’s very easy to create a corresponding MKPlacemark annotation and add that to the map. For example:

 MKPlacemark *mapPlacemark = [[MKPlacemark alloc] initWithPlacemark:placemark];
 [self.mapView addAnnotation:mapPlacemark];

So, replace your alerts with a proper map.

Note

You’ll need to add the Map Kit framework to your project in the same fashion as you added the Core Location framework earlier.

Then right-click the GeoCoder group in the Project Navigator panel and click New File from the menu. Under Cocoa Touch, select a UIViewController subclass, and name it MapViewController when prompted. Ensure the “With XIB for user interface” checkbox is ticked.

Let’s start by creating the user interface for the new map view. Click the MapViewController.xib file to open the nib file in Interface Builder. Drag and drop a navigation bar (UINavigationBar) from Object Library, and position it at the top of the view. Then drag a map view (MKMapView) into the view and resize it to fill the remaining portion of the View window. Finally, drag a bar button item (UIBarButton) onto the navigation bar. In the Attributes Inspector tab of the Utility panel, change its identifier from Custom to Done. Once you’re done this, your view will look similar to Figure 5-8.

Creating your map view in Interface Builder

Figure 5-8. Creating your map view in Interface Builder

After saving the changes to the MapViewController.xib file, open the MapViewController.h interface file. Just as you did for the web view, you want to make this class self-contained so that you can reuse it without any modifications. You’re therefore going to override the init: function to pass the information you need when instantiating the object:

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>

@interface MapViewController : UIViewController <MKMapViewDelegate> {
    NSArray *mapAnnotations;
    IBOutlet MKMapView *mapView;
    IBOutlet UINavigationItem *mapTitle;
}

- (id) initWithPlacemarks:(NSArray *)placemarks;
- (IBAction) done:(id)sender;

@end

Do the same in the corresponding MapViewController.m implementation file:

#import "MapViewController.h"

@interface MapViewController ()

@end

@implementation MapViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {

    }
    return self;
}

- (id) initWithPlacemarks:(NSArray *)placemarks {
    if ( self = [super init] ) {
        mapAnnotations = placemarks;
    }
    return self;
}

- (IBAction) done:(id)sender {
    [self dismissModalViewControllerAnimated:YES];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    for (CLPlacemark *placemark in mapAnnotations) {

        NSLog(@"Placemark is %@", placemark);
        MKPlacemark *mapPlacemark = [[MKPlacemark alloc]
            initWithPlacemark:placemark];
        [mapView addAnnotation:mapPlacemark];

    }

}

- (void)viewDidUnload {
    [super viewDidUnload];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:
        (UIInterfaceOrientation)interfaceOrientation {
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

@end

Save your changes and click the MapViewController.xib nib file to open it in Interface Builder. Right-click and drag from File’s Owner to the title item in the navigation bar and connect it to the mapTitle outlet. Similarly, right-click and drag from File’s Owner to the map view to connect it to the mapView outlet. Finally, right-click and drag from the Done button to File’s Owner and connect it to the done: received action (see Figure 5-9) and from the map view to File’s Owner to connect it as a delegate.

Connecting the outlets and actions

Figure 5-9. Connecting the outlets and actions

Save your changes to the nib file and switch to the ViewController.m implementation file, then import the MapViewController class:

#import "MapViewController.h"

Then replace the code inside the reverseButton: and forwardButton: methods as below:

- (IBAction)reverseButton:(id)sender {

    CLLocation *location =
        [[CLLocation alloc] initWithLatitude:[self.latitude.text doubleValue]
            longitude:[self.longitude.text doubleValue]];

    NSLog(@"location = %@", location);
    CLGeocoder *geocoder = [[CLGeocoder alloc] init];

    [geocoder reverseGeocodeLocation: location
                   completionHandler: ^(NSArray *placemarks, NSError *error) {

                       NSLog(@"completed");
                       if ( error ) {
                           NSLog(@"error = %@", error );
                           dispatch_async(dispatch_get_main_queue(),^{

                               UIAlertView *alert = [[UIAlertView alloc]
                                   initWithTitle:@"Error" message:
                                       [self errorMessage:error.code]
                                   delegate:nil
                                   cancelButtonTitle:nil
                                   otherButtonTitles:@"OK", nil];
                               [alert show];

                           });

                       }
                       MapViewController *mapView = [[MapViewController alloc]
                           initWithPlacemarks:placemarks];
                       [self presentModalViewController:mapView animated:YES];
                   }];
}

- (IBAction)forwardButton:(id)sender {

    NSString *place = self.address.text;
    CLGeocoder *geocoder = [[CLGeocoder alloc] init];

    [geocoder geocodeAddressString:place
                 completionHandler:^(NSArray* placemarks, NSError* error){

        NSLog(@"completed");
        if ( error ) {
            NSLog(@"error = %@", error );
            dispatch_async(dispatch_get_main_queue(),^{

                UIAlertView *alert =
                    [[UIAlertView alloc] initWithTitle:@"Error"
                        message:[self errorMessage:error.code]
                        delegate:nil
                        cancelButtonTitle:nil
                        otherButtonTitles:@"OK", nil];
                [alert show];

            });
        }
        MapViewController *mapView = [[MapViewController alloc]
            initWithPlacemarks:placemarks];
        [self presentModalViewController:mapView animated:YES];
    }];
}

Save your changes and click the Run button in the Xcode tool bar to build and deploy your application. You should see something very much like Figure 5-10.

Entering an address to forward geocode (left) and the resulting annotated map (right)

Figure 5-10. Entering an address to forward geocode (left) and the resulting annotated map (right)

You can slightly improve on that, however. Go back to your viewDidLoad: method inside your MapViewController.m implementation and pull out the first item in the placemark array.

- (void)viewDidLoad {
    [super viewDidLoad];
    for (CLPlacemark *placemark in mapAnnotations) {

        NSLog(@"Placemark is %@", placemark);
        MKPlacemark *mapPlacemark = [[MKPlacemark alloc]
            initWithPlacemark:placemark];
        [mapView addAnnotation:mapPlacemark];

    }

    CLPlacemark *first = (CLPlacemark *)[mapAnnotations objectAtIndex:0];
    MKCoordinateRegion region = { first.location.coordinate , {0.2, 0.2} };

    mapTitle.title = first.locality;
    [mapView setRegion:region animated:NO];

}

Now if you build and deploy the application again, you’ll get something like Figure 5-11.

Entering an address to forward geocode (left), the resulting annotated map (center), and the annotation information (right)

Figure 5-11. Entering an address to forward geocode (left), the resulting annotated map (center), and the annotation information (right)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset