Core Location is a powerful framework that allows your apps to use location information. Mixed with the MapKit, you can create some powerful location based apps. Yesterday I wrote a tutorial about creating a CLRegion so that you can monitor when you enter or exit that particular area and have your app respond accordingly, even when the app is closed.
Today, I want to cover two more of the classes available in Core Location. Today we will focus on the CLGeocode class along with the CLPlacemark class. The reason I combine both in this tutorial, along with the required CLLocationManager and CLLocationManagerDelegate is that the CLGeocode class requires CLPlacemark to make it useful. Likewise, the CLPlacemark, according to documentation, is typically created by a CLGeocoder object.
CLGeocoder and CLPlacemark Tutorial
For the tutorial today, I have created a simple app that will do two things. First, it will use your current location and will tell you the address of where you are along with other information it has about your location. This part is known as reverse-geocoding. Second, we will include a search bar or text field in on the main view where you can type an address and it will show the coordinates in a UILabel as well as move the map and zoom to those coordinates. This part is referred to as forward-geocoding.
A few things to point out before getting in to the tutorial:
1. The CLGeocoder needs an active network connection such as WiFi or 3G/4G to function. All address and coordinate information is stored online and each time you need to use CLGeocoder, you need to put in a request for the information. I think this is partly obvious though because we will be using MapKit which also requires an active connection to load up the map. Some information is offline such as larger searches like towns, but for detailed searches you need a connection.
2. I haven’t included any error checking. For the purpose of this tutorial, I assume you have an iOS device that has location abilities built in and also allows you to use them. I also assume that you will agree to let the app use your current location when asked on the first run.
Lets take a quick look at what is available in each of these classes.
CLPlacemark Class Reference
As with many class references, there is often an initialiser, in this case that is -initWithPlacemark:. You will get to see how this works later on in the tutorial. For now, I want to focus on what information is stored in a CLPlacemark when it is initialised. The list to the left shows 16 properties that are stored with each CLPlacemark. Most of these are self explanatory such as the location property which stores the coordinates of the location. I decided to post the screen shot of the documentation so you can get an idea of what we will be seeing when we create our app.
CLGeocoder Class Reference
The CLGeocoder class provides a way to turn a coordinate in to a friendly representation that people would typically understand… AKA an address. Likewise, it can also convert an address in to a coordinate. There are just 5 methods and 1 property that are available to use in this class. We also learn that forward-geocoding might return more than 1 CLPlacemark. This is because your search might be as simple as “Park” in which case it could list several parks local to your current location.
We have 1 method to reverse geocode a location. That is – (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler. For forward geocoding we have 3 methods available:
– (void)geocodeAddressDictionary:(NSDictionary *)addressDictionary completionHandler:(CLGeocodeCompletionHandler)completionHandler
– (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler
– (void)geocodeAddressString:(NSString *)addressString inRegion:(CLRegion *)region completionHandler:(CLGeocodeCompletionHandler)completionHandler
For the purposes of this test we are going to use the – (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler method when forward-geocoding an address. The other two options are similar. The first method requires a dictionary containing the address. The latter just adds more search functionality to the search and allows you to specify what region you are searching. You do this by creating a CLRegion which as we learn in this tutorial, is a point on a map with a radius. This means we could localise searches to a local town for example.
Creating the App
Lets begin by creating a single view application and when done, drag a map to the lower portion of the screen, drag a UITextField to the top of the screen and a label below that. I also added a toolbar along with a My Location button so we can always jump back to the location. Pushing that will also show your current address and will change if you have moved between button presses. Note that I pushed the Apple Retina 3.5 format button in the storyboard so we don’t have to mess with the layout.
Before moving on, go ahead and add the CoreLocation and MapKit frameworks to your project.
Configuring the Map
As we will need to communicate with the map and make it zoom to our current location as well as zoom to a searched location, we need to connect it up as an outlet.
CTRL+Drag from the map to the header file between the @interface and @end lines.
#import
#import
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@end
At this point we also add the MapKit import line. Also, you can click the map and go to the attributes inspector and check the Shows User Location behaviour.
Connecting up the UITextField, UILabel and UIBarButtonItem
As we are connecting up outlets and actions, we might as well connect the rest of the app to prepare it to function correctly. Go ahead and CRTL+drag from the UITextField to the implementation file. When prompted, leave the options as “Did End on Exit” and select UITextField rather than id. Connect up the UIBarButtonItem to the implementation file and change id to UIBarButtonItem. Finally connect up the UILabel (not the static title we created) to the header file.
Header Code
#import
#import
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (weak, nonatomic) IBOutlet UILabel *myAddress;
@end
Implementation Code
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (IBAction)searchBox:(UITextField *)sender {
}
- (IBAction)myLocation:(UIBarButtonItem *)sender {
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
Lets Create the Code
Because we need to utilise CLLocationManager and the CLLocationManagerDelegate, lets start by adding those to the project. First, import CoreLocation to the header:
#import
Then add the property we need:
@property (strong, nonatomic) CLLocationManager *locationManager;
Finally, add the CLLocationManagerDelegate to the line beginning @interface:
@interface ViewController : UIViewController
In the viewDidLoad method in the implementation lets add this code:
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.delegate = self;
[self.locationManager startUpdatingLocation];
This will fire up the location updates.
Zooming to Our Current Location
The next section of code will get the current location and zoom in to a quarter mile radius.
- (IBAction)myLocation:(UIBarButtonItem *)sender {
float spanX = 0.00725;
float spanY = 0.00725;
self.location = self.locationManager.location;
NSLog(@"%@", self.location.description); //A quick NSLog to show us that location data is being received.
MKCoordinateRegion region;
region.center.latitude = self.locationManager.location.coordinate.latitude;
region.center.longitude = self.locationManager.location.coordinate.longitude;
region.span = MKCoordinateSpanMake(spanX, spanY);
[self.mapView setRegion:region animated:YES];
}
Rather than use the delegate methods to be called automatically, I put this in the myLocation IBAction method. On lines 2 and 3, we set a span which is calculated at 69 miles per degree. That works out to be 0.00725 degrees for half a mile.
Line 4 we get the current location and store it in a new property of type CLLocation called location.
Line 5 we NSLog the data.
Line 6 we create a new MKCoordinateRegion structure called region and in the following 3 lines we assign the latitude, longitude and span to it.
Finally in line 10, we set the region and enable animation. When you click the button, it will zoom in to your current location if available.
Using Reverse-Geocoding to Get the Address
Now that we have more of the basic parts in place, lets look at getting the current location address with CLGeocode which in turn, will store that in CLPlacemark. One thing to take note of is that we need to use blocks here. In simple terms, a block is a bit of code that is passed around as an object. I’ll save the more technical details of blocks to another post. What we do next is create a custom method as follows:
- (void)reverseGeocode:(CLLocation *)location {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
NSLog(@"Finding address");
if (error) {
NSLog(@"Error %@", error.description);
} else {
CLPlacemark *placemark = [placemarks lastObject];
self.myAddress.text = [NSString stringWithFormat:@"%@", ABCreateStringWithAddressDictionary(placemark.addressDictionary, NO)];
}
}];
}
Line 1 – I created a method that returns no value but requires a CLLocation for the argument. We could have done this several ways such as querying the current location within the code. This is just one example way of how it can be done.
Line 2 we create and alloc/init a CLGeocoder. It is important to alloc/init. If you skip this part the reverseGeocodeLocation method on the next line will not run.
Line 3 we run the method on the CLGeocoder supplying it with location taking from the method name. We then use a block which runs as an object on the next few lines.
Lines 5-7 are checking for errors and if present, an NSLog with the error message is created. If no errors then run the code in the else curly braces.
Lines 9 and 10 – Here we create a CLPlacemark and take the lastObject from placemarks and store it in placemark. We then assign a stringWithFormat (a class method from NSString) and access placemark.addressDictionary through the ABCreateStringWithAddressDictionary function. We need to add the AddressBookUI framework and import it in to the header of the implementation file. The reason we use this function is that it makes it easier to format the address with a single call. We could opt to query the name, and many other properties to create a formatted string with linebreaks, but as Apple provides a way with the ABCreateStringWithAddressDictionary function we might as well use that.
To get the app working you just need to add one more call to this particular method. I put this at the bottom of the myLocation method:
[self reverseGeocode:self.location];
This line calls the reverseGeocode method and sends the self.location coordinates along with it.
You can now go ahead and test the app. If you are running on a device, click My Location and it should show your address where Label is, assuming you have a network connection and have allowed location services to run.
If you want to test on the simulator, run the app, click Debug > Location and switch to Apple and then push My Location. You will then see the famous Apple address. Alternatively, put in some custom coordinates and see what address it returns.
Using Forward-Geocoding to Find an Address
Now that reverse-geocoding is working, lets take a look at the alternative option… forward-geocoding. As explained above, here we can type an address and coordinates are returned. What we will do here is have the map move to and zoom to the coordinates provided by the forward-geocoding lookup.
- (IBAction)searchBox:(UITextField *)sender {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder geocodeAddressString:sender.text completionHandler:^(NSArray *placemarks, NSError *error) {
if (error) {
NSLog(@"%@", error);
} else {
CLPlacemark *placemark = [placemarks lastObject];
float spanX = 0.00725;
float spanY = 0.00725;
MKCoordinateRegion region;
region.center.latitude = placemark.location.coordinate.latitude;
region.center.longitude = placemark.location.coordinate.longitude;
region.span = MKCoordinateSpanMake(spanX, spanY);
[self.mapView setRegion:region animated:YES];
}
}];
}
Here is the code I used for this. Two things you might notice. First is that the frame work of this method is almost identical to the reverse geocode. All we do is use a different method and provide an NSString to it… ie, some kind of address. Second, you might notice that the code we use to zoom in to the coordinates almost matches what we use to zoom to our current location. For coding style, I do not recommend you do this. Instead, refactor the code and make it compatible with whatever coordinates and spans are passed to it. That way, if we need to modify this part of code we don’t have to modify the other part of code to match it.
For the purpose of this tutorial, I wanted to show you the basics though and how you work with reverse and forward geocoding.
Now that this method is in place, make sure its wired up correctly to the UI. One thing you might need to change is the send event type for the UITextField. Did End On Exit works for this scenario. I also made a few tweaks to the text field with the attributes inspector such as automatically clearing when you tap the search bar and I changed the keyboard to have a Search button on it.
Running the App
Now that all is in place, you can go ahead and run the app. Note that the Current Address will either show label (meaning you haven’t pushed the My Location button yet), or your current address. The label will not be updated to show the searched for address. You could add that in to the mix if you liked… type a postcode, search for the coordinates and then do a reverse lookup to pull in the address. I’ll leave that to you for an exercise though.
Full code is below for those who get stuck:
Header Code
#import
#import
#import
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (weak, nonatomic) IBOutlet UILabel *myAddress;
@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong, nonatomic) CLLocation *location;
@end
Implementation Code
#import "ViewController.h"
#import
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.delegate = self;
[self.locationManager startUpdatingLocation];
}
- (IBAction)searchBox:(UITextField *)sender {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder geocodeAddressString:sender.text completionHandler:^(NSArray *placemarks, NSError *error) {
if (error) {
NSLog(@"%@", error);
} else {
CLPlacemark *placemark = [placemarks lastObject];
float spanX = 0.00725;
float spanY = 0.00725;
MKCoordinateRegion region;
region.center.latitude = placemark.location.coordinate.latitude;
region.center.longitude = placemark.location.coordinate.longitude;
region.span = MKCoordinateSpanMake(spanX, spanY);
[self.mapView setRegion:region animated:YES];
}
}];
}
- (IBAction)myLocation:(UIBarButtonItem *)sender {
float spanX = 0.00725;
float spanY = 0.00725;
self.location = self.locationManager.location;
MKCoordinateRegion region;
region.center.latitude = self.locationManager.location.coordinate.latitude;
region.center.longitude = self.locationManager.location.coordinate.longitude;
region.span = MKCoordinateSpanMake(spanX, spanY);
[self.mapView setRegion:region animated:YES];
[self reverseGeocode:self.location];
}
- (void)reverseGeocode:(CLLocation *)location {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
NSLog(@"Finding address");
if (error) {
NSLog(@"Error %@", error.description);
} else {
CLPlacemark *placemark = [placemarks lastObject];
self.myAddress.text = [NSString stringWithFormat:@"%@", ABCreateStringWithAddressDictionary(placemark.addressDictionary, NO)];
}
}];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
Mazen Kasser says
Best reverse geocode and forward geocode tutorial. Thanks a lot
Matthew says
No problem :)
Mohammed Imran says
Hi I’m really new to this iOS app development trying to develop an application from the past 4 months. I’m stuck in using the mapKit to achieve the following.
Is there any way in iOS to get possible address returned by CLLocation Manager as a drop during runtime. I mean the time when the types in the address in the search Bar should give the possible matching addresses as a dropdown selecting a particular one from a dropdown should be annotated.
And even I’m Trying to limit the service within a specific area. Please do suggest me some ideal will be much appreciated Thanks.
Oscar says
Simply and effective. Thanks a lot!
Sheik says
Thanks
Mythri Shenoy says
Best MAPKit tutorial ever!! Keep going.
Riyaz says
Good Working Tutorial
Thanks>>>>>>
ayush says
hi, Where is source code ?
raj says
how to stop updating placemarks for places?
snehal says
nice tutuorial
Arya says
Hi,
Is there any way to show the exact location using this?