Quantcast
Channel: NSCookbook.com» Xcode 5
Viewing all articles
Browse latest Browse all 6

iOS Programming Recipe 31: Implementing Geocoding

$
0
0

Sometimes it can be handy to either translate an address into a set of coordinates, or get an address from a set of coordinates. These processes are known as geocoding and reverse geocoding respectively. This quick tutorial will go over how to do these things using location services.

Assumptions

  • You can navigate through Xcode; If not, start here
  • You can set up interfaces and connect actions and outlets

Setting Up the Application

Start with a single view application and name it “Recipe 31 Geocoding”. Since we’ll be using the Core Location framework we’ll need to do a couple things to make sure everything works well. The first thing you’ll need to do is add the Core Location framework. Then you will need to add an entry to the .plist file to let the application know you’re using location services.

Adding the Framework

To add the framework, select the root node and find the “Linked Frameworks and Libraries” section on the General tab. Once you have found that section press the “+” button and select and add “CoreLocation.framework”. Figure 1 shows this step.

Adding the Framework

Figure 1: Selecting and adding a framework

Next, open the ViewController.h file and import the the framework as shown in code chunk 1.

Code Chunk 1: Importing the framework


1
2
3
4
5
6
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>

@interface ViewController : UIViewController

@end

Adding an Entry to the Info.plist File

Next select the Info.plist file found under the Supporting Files folder. This file will start with the project name, for example, “Recipe 31 Geocoding-Info.plist”. To create a new entry press the small “+” button at the top of the list next to the words “Information Property List”. Choose “Privacy – Location Usage Description” and type in a description of what the app is. In this case, you can enter something like “Using Geocoding” as shown in Figure 2. Make sure the type is set to string when you do this.

Modifying the plist

Figure 2: Adding an entry to the info.plist file

This entry serves two purposes. For one it provides the user a prompt when location services are being accessed. The text in the prompt will show the description “Using Geocoding”. The other reason to do this is for Apple’s use in categorizing your app; without this entry, Apple will probably not accept your app.

Building the Interface

Now that we have the info.plist and framework taken care of, let’s build the interface. The interface will be a pretty simple one. We’ll need one label, a multi-line label, one text field, and two buttons. Arrange them as shown in Figure 3. Make sure you resize the multi-line label so it has space to grow. You can change the number of lines in the label from the Attributes inspector.

Setting up the interface

Figure 3: Setting up the interface

Now create outlets for the multi-line label and the text field with the values “outputLabel” and “inputText”.

Create a two button actions titles “findAddress” and “findLocation”.

When you are done your code interface should look like the Code Chunk 2. I have chosen to add these to ViewController.h file, but you can also add them to the ViewController.m file if you wish. You can also see that I made this class conform to the CLLocationManagerDelegate protocol. This will be necessary to recieve errors and updated locations.

Code Chunk 2: The completed ViewController.h file


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

@interface ViewController : UIViewController <CLLocationManagerDelegate>

@property (weak, nonatomic) IBOutlet UILabel *outputLabel;
@property (weak, nonatomic) IBOutlet UITextField *inputText;
- (IBAction)findAddress:(id)sender;
- (IBAction)findLocation:(id)sender;

@end

Fun With Geocoding

We’ll start with forward geocoding, both because it makes since to go forward before going backward, and it’s a bit easier. To start with, you’ll want to add a new property to the ViewController.h file as shown in Code Chunk 3. This will give us a place to store an instance of the geocoder.

Code Chunk 3: Adding a geocoder property to the ViewController.h file


1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>

@interface ViewController : UIViewController <CLLocationManagerDelegate>

@property (weak, nonatomic) IBOutlet UILabel *outputLabel;
@property (weak, nonatomic) IBOutlet UITextField *inputText;

@property (strong, nonatomic) CLGeocoder *geocoder;

- (IBAction)findAddress:(id)sender;
- (IBAction)findLocation:(id)sender;

@end

Now we can start editing the find location method. To be clear, this method will recieve an address that is typed-in and translate it into coordinates.

First thing you will want to do is check to see if the geocoder has been initialized. If not, you will go ahead and initialize it. Then you want implement the geocodeAddressString:completionHandler: method. This method will take the address and return placemarks for the address, or an error if there is one. Code Chunk 4 shows the success case and the initialization check. You can see from this code that you take the first object returned from placemarks array and output that placemark location desciption.

Code Chunk 4: Checking for geocoder initialization and handling of a completion success case


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (IBAction)findLocation:(id)sender
{
    //check to see if geocoder initialized, if not initialize it
    if(self.geocoder == nil)
    {
        self.geocoder = [[CLGeocoder alloc] init];
    }

    NSString *address = self.inputText.text;
    [self.geocoder geocodeAddressString:address completionHandler:^(NSArray *placemarks, NSError *error) {

        if(placemarks.count > 0)
        {
            CLPlacemark *placemark = [placemarks objectAtIndex:0];
            self.outputLabel.text = placemark.location.description;
        {    
    }]    
}

Now, we’ll do an else if statement to check for domain errors. This will include a case statement that will look for a number of errors. Code Chunk 5 shows this change.

Code Chunk 5: Handling geocoder domain errors
– (IBAction)findLocation:(id)sender
{
if(self.geocoder == nil)
{
self.geocoder = [[CLGeocoder alloc] init];
}
NSString address = self.inputText.text;
[self.geocoder geocodeAddressString:address completionHandler:^(NSArray
placemarks, NSError *error) {


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
            if(placemarks.count > 0)
            {
                CLPlacemark *placemark = [placemarks objectAtIndex:0];
                self.outputLabel.text = placemark.location.description;
            }
            else if (error.domain == kCLErrorDomain)
            {
                switch (error.code)
                {
                    case kCLErrorDenied:
                        self.outputLabel.text
                        = @"Location Services Denied by User";
                        break;
                    case kCLErrorNetwork:
                        self.outputLabel.text = @"No Network";
                        break;
                    case kCLErrorGeocodeFoundNoResult:
                        self.outputLabel.text = @"No Result Found";
                        break;
                    default:
                        self.outputLabel.text = error.localizedDescription;
                        break;
                }
            }

        }];

    }

As you can see from this bit of code, we are checking for the folloiwng errors:

  • User denied location services
  • Network availability
  • No result from address input

All of these errors will be updated in the output label if they occur. Since we’re not checking for all possible errors, we also added a default case to output any other error.

The last thing we’ll do to this method is add one more else case to catch any other error. Eg, non domain errors. Code Chunk 6 shows the complete method.

Code Chunk 6: The complete findLocation method


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- (IBAction)findLocation:(id)sender
{
    if(self.geocoder == nil)
    {
        self.geocoder = [[CLGeocoder alloc] init];
    }
    NSString *address = self.inputText.text;
    [self.geocoder geocodeAddressString:address completionHandler:^(NSArray *placemarks, NSError *error) {

        if(placemarks.count > 0)
        {
            CLPlacemark *placemark = [placemarks objectAtIndex:0];
            self.outputLabel.text = placemark.location.description;
        }
        else if (error.domain == kCLErrorDomain)
        {
            switch (error.code)
            {
                case kCLErrorDenied:
                    self.outputLabel.text
                    = @"Location Services Denied by User";
                    break;
                case kCLErrorNetwork:
                    self.outputLabel.text = @"No Network";
                    break;
                case kCLErrorGeocodeFoundNoResult:
                    self.outputLabel.text = @"No Result Found";
                    break;
                default:
                    self.outputLabel.text = error.localizedDescription;
                    break;
            }
        }
        else
        {
            self.outputLabel.text = error.localizedDescription;

        }

    }];

}

If you build and run your project now, you can enter an address into the text field and click the “Find Location” button. You should get the coordinates for that address.

You have several options for entering the address. You can enter one of the following:

  • The name of a landmark
  • just the street name eg. 123 some street
  • The full address 123 some street, city, ST COUNTRY

Figure 4 shows the coordinates for the Empire State Building.

Simulating forward geocoding

Figure 4: Forward Geocoding Simulation

Reverse Geocoding

Ok, Let’s kick this up a notch. Now we’ll turn our focus over to the findAddress method. This method will be pretty simple. Basically, we’ll initialize the location manager and set the distance filter, delegate, and desired accuracy if not already done. Then we’ll start location updates and output an error if location services aren’t enabled. Code Chunk 7 shows all of this.

Code Chunk 7: Implementing the findAddress method


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (IBAction)findAddress:(id)sender
{

    if([CLLocationManager locationServicesEnabled])
    {
        if(self.locationManager == nil)
        {
            self.locationManager = [[CLLocationManager alloc] init];
            self.locationManager.distanceFilter = 300;
            self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
            self.locationManager.delegate = self;

        }

        [self.locationManager startUpdatingLocation];
        self.outputLabel.text= @"Getting your address";
    }
    else
    {

        self.outputLabel.text = @"Location Services Not Available";

    }
}

A couple of notes on this bit of code. The distance filter is the minimum distance a device must move horizontally before and update event is triggered. The desired accuracy is pretty self explainitory. There are several accuracy types choose from. As a best practice, don’t use anything more accurate than you actually need. This is taxing on the battery. Since I want a pretty accurate location, I chose Best accuracy.

Next, we’ll need to implement a couple of delegates. The first one is easy, it basically just lets us know when the location manager fails. So we’ll just set our output label to the error if this occurs (Code Chunk 8).

Code Chunk 8: Implementing the locationManager:didFailWithError: delegate method


1
2
3
4
5
6
7
-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    if(error.code == kCLErrorDenied)
    {
        self.outputLabel.text = @"Location information denied";
    }
}

The next delegate method is not so straight forward, So we’ll go through it one step at a time.

The first thing you’ll want to do is retrieve the location when it updates and check that it is a recent location event. You’ll recieve the location updates using the locationManager: didUpdateLocations: delegate method as shown in Code Chunk 9.

Code Chunk 9: Setting up the locationManger: didUpdateLocations: delegate method


1
2
3
4
5
6
7
8
9
10
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    // Make sure the location is recent
    CLLocation *newLocation = [locations lastObject];
    NSTimeInterval eventInterval = [newLocation.timestamp timeIntervalSinceNow];
    if(abs(eventInterval) < 30.0)
    {

    }
}

Next you’ll want to check and make sure the location is accurate, and make sure the geocoder is initialized and not currently geocoding as shown in Code Chunk 10.

Code Chunk 10: Checking for valid location, initializing geocoder, and checking existance of geocoder


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    // Make sure this is a recent location event
    CLLocation *newLocation = [locations lastObject];
    NSTimeInterval eventInterval = [newLocation.timestamp timeIntervalSinceNow];
    if(abs(eventInterval) < 30.0)
    {
        // Make sure the event is valid
        if (newLocation.horizontalAccuracy < 0)
        {
            return;
        }


        // Instantiate _geoCoder if it has not been already
        if (self.geocoder == nil)
        {
            self.geocoder = [[CLGeocoder alloc] init];
        }


        //Only one geocoding instance per action
        //so stop any previous geocoding actions before starting this one
        if([self.geocoder isGeocoding])
        {
            [self.geocoder cancelGeocode];
        }

    }
}

Now you will implement a new completion block which will actually perform the the reverse geocode lookup. This portion will look nearly the same as forward geocoding. The only difference is you will now take the placemark and format a string to include the placemark description. Of course, the errors are a little bit different as well.

Code Chunk 11: Adding the reverse geocode completion block


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    // Make sure this is a recent location event
    CLLocation *newLocation = [locations lastObject];
    NSTimeInterval eventInterval = [newLocation.timestamp timeIntervalSinceNow];
    if(abs(eventInterval) < 30.0)
    {
        // Make sure the event is valid
        if (newLocation.horizontalAccuracy &lt; 0)
        {
            return;
        }


        // Instantiate _geoCoder if it has not been already
        if (self.geocoder == nil)
        {
            self.geocoder = [[CLGeocoder alloc] init];
        }


        //Only one geocoding instance per action
        //so stop any previous geocoding actions before starting this one
        if([self.geocoder isGeocoding])
        {
            [self.geocoder cancelGeocode];
        }


        [self.geocoder reverseGeocodeLocation: newLocation
                        completionHandler: ^(NSArray* placemarks, NSError* error)
         {
             if([placemarks count] > 0)
             {
                 CLPlacemark *foundPlacemark = [placemarks objectAtIndex:0];
                 self.outputLabel.text =
                 [NSString stringWithFormat:@"You are in: %@",
                  foundPlacemark.description];
             }
             else if (error.code == kCLErrorGeocodeCanceled)
             {
                 NSLog(@"Geocoding cancelled");
             }
             else if (error.code == kCLErrorGeocodeFoundNoResult)
             {
                 self.outputLabel.text=@"No geocode result found";
             }
             else if (error.code == kCLErrorGeocodeFoundPartialResult)
             {
                 self.outputLabel.text=@"Partial geocode result";
             }
             else
             {
                 self.outputLabel.text =
                 [NSString stringWithFormat:@"Unknown error: %@",
                  error.description];
             }
         }
         ];

    }
}

Finally, all you have to do is stop the location manager updates. Code Chunk 12 shows this last step. Part of the method has been omitted for brevity.

Code Chunk 12: Adding code to stop the location manager updates


1
2
3
4
5
6
7
8
9
10
11
12
  //...
                 self.outputLabel.text =
                 [NSString stringWithFormat:@"Unknown error: %@",
                  error.description];
             }
         }
         ];

        //Stop updating location until they click the button again
        [manager stopUpdatingLocation];
    }
}

now if you build and run the application it should be fully functional.

simulating reverse geocoding

Figure 5: The final application

While this app is pretty simple, it does illustrate how powerful geocoding can be. Hopefully you can figure out a better way to use geocoding in your apps. Enjoy!


Viewing all articles
Browse latest Browse all 6

Trending Articles