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.
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.
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
)
{
for
(
CLPlacemark
*
placemark
in
placemarks
)
{
NSLog
(
@"Placemark is %@"
,
placemark
);
}
}];
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
,
and LONG
).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 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
){
for
(
CLPlacemark
*
placemark
in
placemarks
)
{
NSLog
(
@"Placemark is %@"
,
placemark
);
}
}];
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.
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.
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).
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;}
@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
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
{
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
;
}
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).
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.
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).
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).
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).
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.
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.
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.
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.
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.
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.