Chapter     5

Location Recipes

Knowing a device’s correct location and heading has enabled developers to take apps to a whole new level. Apps that utilize location services in new ways keep showing up, and the potential uses seem endless. You can build entire apps around these features, such as routing or fitness tracking apps, or you can use location services to enhance the user’s experience in more traditional apps. A good example of the latter is the built-in Reminders app, which can remind you of certain tasks when you reach a certain location, such as calling a friend when you get home.

Apple has been paying a lot of attention to this field lately, and iOS 7 offers better accuracy and better availability than its predecessor, as well as some new default behaviors that will improve battery life. With iOS 7, you are better equipped than ever to make your apps location-aware. This chapter shows you how to get started.

About Core Location

The Core Location framework has everything you need to implement location awareness into your app. In particular, it supports the following:

  • Location tracking
  • Monitoring significant location changes
  • Monitoring entrances or exits of custom regions
  • Getting the current heading
  • Translating coordinates into addresses (forward geocoding)
  • Translating addresses into coordinates (reverse geocoding)

To do its job, Core Location utilizes data from several sources, including cellular masts, nearby wi-fi networks, GPS, and the magnetometer. There is some really complex stuff going on inside that we don’t have to care about because the framework encapsulates it into an easy-to-use, all-in-one-place API.

Standard and Significant Change Services

There are two primary methods for finding the location of a device: the standard location serviceand the significant location change service. Which one you use depends on how accurate you need that information to be and how often you need to be notified that a device’s location has changed.

The standard location service provides more accurate location information and invokes the GPS if the requested accuracy requires it. This greater accuracy comes at a cost in terms of a longer time to get a location and an increased drain of the battery. If you are going to use the standard location service, you should use it with precision and only when necessary.

The significant location change service provides some flexibility and is recommended for most applications that don’t need highly accurate location information. For instance, if you need to know the town or city where someone is located, the significant location change service is perfectly acceptable. You get a fast response without using a lot of battery power because it uses the cellular signal to determine the device’s location. Another benefit of the significant location change service is it runs in the background on the device. Your app does not have to be running in the foreground to receive location updates from this service, unlike the standard location service.

Recipe 5-1: Getting Basic Location Information

This recipe shows you how to use the standard location service to give you some basic information about the device’s current location, course, and speed.

Setting Up the Application

Start by creating a new single view application and add the Core Location framework to the project. (See Chapter 1 for instructions about how to create the project and link the framework library).

You will use a very simple user interface with a title label, a text view to display location information, and a switch control to let the user turn location updates on and off. Bring up the storyboard by selecting the Main.storyboard file in the project navigator. Add the label and the switch to your single view by dragging them from the object library. Make the text view big enough to contain four rows. Likewise, make sure the initial state of the switch is set to “Off.”

Your user interface should now resemble the one in Figure 5-1.

9781430259596_Fig05-01.jpg

Figure 5-1. User interface for Recipe 5-1

Now create outlet properties for the text view and the switch. Name them “locationInformationView” and “locationUpdatesSwitch.” You also need to know when the user taps on the switch control, so go ahead and create an action for the switch. Name it “toggleLocationUpdates” and set the event type to “Value Changed.” (If you are uncertain of outlets and actions, you can find instructions about how to create them in Chapter 1).

All interaction with the Core Location framework goes through a location manager. With it you can start and stop the location updates. For convenience, set the view controller to be the location manager’s delegate; it is the “hub” of action for dealing with all location-based services. Therefore, add CLLocationManagerDelegate as a supported protocol of the view controller’s class, as well as the import statement shown in Listing 5-1.

Listing 5-1.  Declaring use of the CLLocationManagerDelegate and importing the CoreLocation framework

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

@interface ViewController : UIViewController <CLLocationManagerDelegate>

// ...

@end

Now you need an instance variable for your location manager. Add it to the view controller as well, and name it “_locationManager” (See Listing 5-2).

Listing 5-2.  Setting an instance variable for the location manager

// ...

@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}

// ...

@end

Your view controller’s header file should now look something like Listing 5-3.

Listing 5-3.  The ViewController.h file with Listings 5-1 and 5-2 added

//
//  ViewController.h
//  Recipe 5-1 Getting Basic Location Info
//

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

@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}

@property (weak, nonatomic) IBOutlet UITextView *locationInformationView;
@property (weak, nonatomic) IBOutlet UISwitch *locationUpdatesSwitch;

- (IBAction)toggleLocationUpdates:(id)sender;

@end

Finally, because you are planning to use the location services, you should provide a purpose description. This is done in the application’s Info.plist file. Add the key “NSLocationUsageDescription” with the value “We're testing standard location services” (without the quotes), as shown in Figure 5-2. When a user is prompted to allow your application access to his location, that text is displayed, telling the user what you plan to do with his device’s location information. Once you type in “NSLocationUsageDescription” the key will change to “Privacy – Location Usage Description” automatically.

9781430259596_Fig05-02.jpg

Figure 5-2. Setting the location usage description

Your application skeleton is ready, and now is a good time to build and run it. Nothing interesting will happen, though, when you interact with its user interface; you have yet to implement the code that starts and stops location services, as well as the one that receives the location updates. Let’s get started.

Starting and Stopping Location Updates

Now that the interface has been defined, move to the view controller’s implementation file (.m) and start implementing these methods and objects. The first thing you will tackle is the toggleLocationUpdates: action, which is invoked when the user touches the switch control.

You need to take a different action depending on whether the user turned the updates on or off, obviously. If the switch was turned on, you should check whether location services are enabled. If they are not, display an alert and turn the switch back to off. Add the code to the toggleLocationUpdates: method, as shown in Listing 5-4.

Listing 5-4.  Adding code to toggleLocationUpdates to create a user alert

- (IBAction)toggleLocationUpdates:(id)sender
{
    if (self.locationUpdatesSwitch.on == YES)
    {
        if ([CLLocationManager locationServicesEnabled] == NO)
        {
            UIAlertView *locationServicesDisabledAlert =
                [[UIAlertView alloc] initWithTitle:@"Location Services Disabled"
                message:@"This feature requires location services. Enable it in the privacy settings on your device"
                delegate:nil
                cancelButtonTitle:@"Dismiss"
                otherButtonTitles:nil];
            [locationServicesDisabledAlert show];
            self.locationUpdatesSwitch.on = NO;
            return;
        }

        // ...

    }
    else
    {
        // Switch was turned off
    }
}

Next, if location services are enabled, initialize the location manager if it hasn’t been previously initialized. For the standard location service, you should always set the desiredAccuracy and distanceFilter properties of the location manager. Also, it’s recommended that you set the activityType.

The desiredAccuracy property tells the Core Location framework how accurate (in meters) you want your location information to be. The accuracy, however, is not guaranteed, and the device will try to use the resources available to it to get information as close to your desired accuracy as possible. Apple recommends that you be as conservative as possible with this setting. If you don’t need to know the street address of the current device, use a lower accuracy setting. Here are all of the constants available:

  • kCLLocationAccuracyBestForNavigation
  • kCLLocationAccuracyBest
  • kCLLocationAccuracyNearestTenMeters
  • kCLLocationAccuracyHundredMeters
  • kCLLocationAccuracyKilometer
  • kCLLocationAccuracyThreeKilometers

Note   If you are not familiar with the metric system, a meter (m) is slightly longer than a yard (1 yard = 0.9144 m), and a kilometer (km) is just over half a mile (1 mile = 1.609 km).

The distanceFilter property is how far a device has to move (again in meters) before you want to be notified (through your delegate) of its new position. The only constant provided for this property is kCLDistanceFilterNone , which reports all changes in location to your delegate.

The activityTypeis used by the Core Location framework to better figure out when it should autopause location updates. So if you chose the type CLActivityTypeFitness, Core Location might pause when the runner stops running for a certain amount of time. You have four activity types to choose from:

  • CLActivityTypeAutomotiveNavigation: This type is best used for navigation in a car. Location updates will pause if no significant change in distance occurs for a while.
  • CLActivityTypeFitness: This type is best suited for walking or running. The behavior is the same as ClActivityTypeAutomotiveNavigation, except the significant amount of distance is much smaller in the same time period.
  • CLActivityTypeOtherNavigation: This works the same as CLActivityTypeAutomotiveNavigation, but is more tailored for train, boat, or plane travel.
  • CLActivityTypeOther: Use this type if the other types don’t fit your needs.

Once you’ve set the properties and the delegate in the ViewController.m file, you can start the location services by calling the startUpdatingLocation method on your CLLocationManager, a class for managing the delivery of location and heading updates. Do this by adding the code shown in Listing 5-5.

Listing 5-5.  Modify the toggleLocationUpdates method to start updating the location

if (self.locationUpdatesSwitch.on == YES)
{
    if ([CLLocationManager locationServicesEnabled] == NO)
    {
        // ...
    }

    if (_locationManager == nil)
    {
        _locationManager = [[CLLocationManager alloc] init];
        _locationManager.desiredAccuracy = kCLLocationAccuracyBest;
        _locationManager.distanceFilter = 1; // meter
        _locationManager.activityType = CLActivityTypeOther;
        _locationManager.delegate = self;
    }

    [_locationManager startUpdatingLocation];

}
else
  // ...

That concludes the code used when the user turns updates on. For the “off” part, you simply want to stop the updates if they have been started, as shown in Listing 5-6.

Listing 5-6.  Modifying the toggleLocationUpdates method to handle the switch in an “off” position

- (IBAction)toggleLocationUpdates:(id)sender
{
    if (self.locationUpdatesSwitch.on == YES)
    {
        // ...
    }
    else
    {
        // Switch was turned off
        // Stop updates if they have been started
        if (_locationManager != nil)
        {
            [_locationManager stopUpdatingLocation];
        }
    }
}

Receiving Location Updates

The delegate methods need to be set up next. These methods are called when a location update is received or when there is an error getting the location. Let’s start with the error delegate method (in other words, locationManager:didFailWithError:).

The most common source of an error occurs when the user is prompted to allow location services for your app and the user declines. If this happens, you can stop the updates by turning the switch back to off. This triggers a Value Changed event and thus invokes the toggleLocationUpdates: method, which turns the updates off.

For any other error, log it to the console. Your code should look something like Listing 5-7.

Listing 5-7.  Implementing the locationManager: didFailWithError: delegate method

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    if (error.code == kCLErrorDenied)
    {
        // Turning the switch to off will trigger the
        // toggleLocationServices action,
        // which in turn will stop further updates from coming
        self.locationUpdatesSwitch.on = NO;
    }
    else
    {
        NSLog(@"%@", error);
    }
}

The delegate method that handles location updates is a little more involved. The method, locationManager:didUpdateLocations:, delivers an array of locations that have been registered since the last update, the most recent last. For this recipe, you’re only interested in the most recent event, so just extract that one, as shown in Listing 5-8.

Listing 5-8.  Starting the implementation of the locationManager:didUpdateLocations: delegate method  

- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations
{
    CLLocation *lastLocation = [locations lastObject];
    // ...
}

The location is represented by a CLLocation object, which contains a lot of valuable information, including the location coordinate, accuracy information, and the timestamp of the location update.

Before your app processes a location object, you should check whether the timestamp of the location object is recent. Core Location often presents the last known location as the first call to the delegate method before it has a lock on the new location. There is often no need to process a location object that represents the device’s location at some point in history when you need to know where it is now. Therefore, filter out location events that are more than 30 seconds old, as shown in Listing 5-9.

Listing 5-9.  Updating locationManager:didUpdateLocations: to include logic for filtering older locations

- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations
{
    CLLocation *lastLocation = [locations lastObject];
    // Make sure this is a recent location event
    NSTimeInterval eventInterval = [lastLocation.timestamp timeIntervalSinceNow];
    if(abs(eventInterval) < 30.0)
    {
        // This is a recent event
    }
}

The other property you need to check before you process an event is its accuracy. Again, there is no need to process an event if it is not within the accuracy bounds that you are expecting. It might be better to wait for the device to obtain a more accurate reading than to present bad information to the user. The location object contains two accuracy properties: horizontalAccuracy and verticalAccuracy.

The horizontalAccuracy property represents the radius of the circle, in meters, within which the location could be located. You can see this circle in the built-in Maps application when you are showing your location. A negative value indicates that the coordinate is invalid.

The verticalAccuracy property is how far, plus or minus in meters, the altitude of the device could be off. Again, a negative value indicates an invalid altitude reading. If the device does not have a GPS, the verticalAccuracy property will always be negative because a GPS is needed to determine the device’s altitude.

Listing 5-10 is the code extended with a check that the received location’s horizontal accuracy is not invalid and is within 20 meters.

Listing 5-10.  Updating locationManager:didUpdateLocations: to check for poor accuracy

- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations
{
    // Make sure this is a recent location event
    CLLocation *lastLocation = [locations lastObject];
    NSTimeInterval eventInterval = [lastLocation.timestamp timeIntervalSinceNow];
    if(abs(eventInterval) < 30.0)
    {
        // Make sure the event is accurate enough
        if (lastLocation.horizontalAccuracy >= 0 &&
            lastLocation.horizontalAccuracy < 20)
        {
            §self.locationInformationView.text = lastLocation.description;
        }
    }
}

The description property of a location object returns all the information in one dense string. It is a very easy method for seeing what location information is being returned by the device. We don’t recommend showing this string to the end user directly as it contains a great deal of information, but it could be useful for debugging and verifying that location information is being updated and is correct or accurate. For this project, you have set your locationInfomationView text to the lastLocation.description value, resulting in the preceding completed delegate method.

Your application is now finished and ready for testing.

Testing Location Updates

The iOS simulator contains several convenient ways to test location events. As Figure 5-3 shows, there are functions for setting a custom location or simulating different scenarios, such as a city run or a freeway drive.

9781430259596_Fig05-03.jpg

Figure 5-3. Simulating location events

Launch the app on the iOS simulator. When you touch the switch and turn it to “On,” you will be prompted to allow this application access to your device’s location. Notice in Figure 5-4 that the string you set in the application’s property list is displayed.

9781430259596_Fig05-04.jpg

Figure 5-4. Your application requesting location permissions

Click “OK.” Notice that the location information label is not updating even though the switch is on. This is because you haven’t started any location simulations yet. In the iOS simulator, go to the menu Debug arrow.jpg Location arrow.jpg Freeway Drive, and the label should start to update with information about the prerecorded drive that Apple has provided. Figure 5-5 shows a sample of information delivered by the simulated drive.

9781430259596_Fig05-05.jpg

Figure 5-5. Displaying simulated location information

That concludes Recipe 5-1. The next recipe deals with a way to get location changes that require a lot less battery power.

Recipe 5-2: Significant Location Changes

Being a location-aware app doesn’t always mean that it needs the high-accuracy location update that the standard location service provides. Many apps do just fine by knowing which town, city, or even country the device is currently in. For those apps, the significant location change service is the preferred way to retrieve locations; it is faster, requires significantly less battery power, and can run in the background.

This recipe shows you how to set up an application to use the significant location change service to get locations.

Setting Up the Application

Programming the significant location changes services is a lot like programming the standard location services, and the setup is virtually identical. You can either duplicate the project from Recipe 5-1 or create a new single view application and do the following as preparation:

  1. Link the application to the Core Location framework.
  2. Set a usage description (such as “Testing the significant location change service”) for the NSLocationUsageDescription Info.plist key (“PrivacyLocation Usage Description” in the property list).
  3. Add a label, text view,  and a switch control to the main view, which should look something like Figure 5-1 in Recipe 5-1. The text view should contain about five lines and the switch should be initially set to “Off.”
  4. Create outlets for the text view and the switch and name them “locationInformationView” and “locationUpdatesSwitch,” respectively.
  5. Create an action for the switch, name it “toggleLocationUpdates,” and set the event type to “Value Change.”
  6. Import the Core Location framework API by adding the following declaration in your view controller’s header file: #import <CoreLocation/CoreLocation.h>.
  7. Make the view controller a location manager delegate by adding the CLLocationManagerDelegate protocol to the ViewController class.
  8. Finally, add a CLLocationManager * instance variable to the view controller and name it “_locationManager.”

Refer to Recipe 5-1 for the details regarding the preceding steps. Your view controller’s header class should now look like Listing 5-11.

Listing 5-11.  The starting setup for the view controller header file

//
//  ViewController.h
//  Recipe 5-2: Significant Location Changes
//

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

@interface ViewController : UIViewController <CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}

@property (strong, nonatomic) IBOutlet UITextView *locationInformationView;
@property (strong, nonatomic) IBOutlet UISwitch *locationUpdatesSwitch;

- (IBAction)toggleLocationUpdates:(id)sender;

@end

Build and run the app to make sure everything is ready for the next step.

Enabling Background Updates

For this recipe you will enable location updates to occur even when your app is residing in background mode. To do this you need to add another key to the Info.plist file, so type in the “UIBackgroundModes” key (or “Required background modes,” as Xcode translates it to in the user interface). Make sure you set the type to “array” for this item. Next add a sub item with the value “App registers for location updateswith astringtype, as in Figure 5-6.

9781430259596_Fig05-06.jpg

Figure 5-6. Specifying location changes as a required background mode

Note   For the most part, Apple does not want you to use background location tracking unless it is absolutely essential for the functionality of the application. They absolutely do not want you to start location services in the background, but they make an exception when it comes to significant location changes.

Now switch focus to the implementation file (.m) of the view controller. Start with the toggleLocationUpdates: method. It gets invoked each time the user changes the value of the switch control.

You’ll recognize a lot of the code because it’s more or less the same as in Recipe 5-1. The only difference is that you use start and stopMonitoringSignificantLocationChanges instead of start and stopUpdatingLocation. Also, the significant location change service doesn’t care about the desiredAccuracy, distanceFilter, and activityType properties, so you can leave them out.

Listing 5-12 shows the new toggleLocationUpdates: method, with differences marked in bold. Go ahead and implement it in your project.

Listing 5-12.  Implementing the toggleLocationUpdates method

- (IBAction)toggleLocationUpdates:(id)sender
{
    if (self.locationUpdatesSwitch.on == YES)
    {
        if ([CLLocationManager locationServicesEnabled] == NO)
        {
            UIAlertView *locationServicesDisabledAlert = [[UIAlertView alloc]
              initWithTitle:@"Location Services Disabled"
              message:@"This feature requires location services. Enable it in the privacy settings on your device"
              delegate:nil
              cancelButtonTitle:@"Dismiss"
              otherButtonTitles:nil];
            [locationServicesDisabledAlert show];
            self.locationUpdatesSwitch.on = NO;
            return;
        }

        if (_locationManager == nil)
        {
            _locationManager = [[CLLocationManager alloc] init];
            // Significant location change service does not use desiredAccuracy,
            // distanceFilter or activityType properties so no need to set them
            _locationManager.delegate = self;

        }
        [_locationManager startMonitoringSignificantLocationChanges];
    }
    else
    {
        // Switch was turned off
        // Stop updates if they have been started
        if (_locationManager != nil)
        {
            [_locationManager stopMonitoringSignificantLocationChanges];
        }
    }
}

Now you have to set up the delegate methods. They are identical to what you did in the preceding recipe. Listing 5-13 shows the implementation for the locationManager:didFailWithError method.

Listing 5-13.  Implementing the locationManager:didFailWithError: delegate method

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    if (error.code == kCLErrorDenied)
    {
        // Turning the switch to off will trigger the toggleLocationServices action,
        // which in turn will stop further updates from coming
        self.locationUpdatesSwitch.on = NO;
    }
    else
    {
        NSLog(@"%@", error);
    }
}

Listing 5-14 shows the implementation for the locationManager:didUpdateLocations: method.

Listing 5-14.  Implementing the locationManager:didUpdateLocations: delegate method

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    // Make sure this is a recent location event
    CLLocation *lastLocation = [locations lastObject];
    NSTimeInterval eventInterval = [lastLocation.timestamp timeIntervalSinceNow];
    if(abs(eventInterval) < 30.0)
    {
        // Make sure the event is accurate enough
        if (lastLocation.horizontalAccuracy >= 0 &&
            lastLocation.horizontalAccuracy < 20)
        {
            self.locationInformationView.text = lastLocation.description;
        }
    }
}

The app is nearly finished, so you can build and test it now. Again, you can simulate the location by going to the simulator menu Debug • Location • Freeway Drive. The application text should update when a significant location change has occurred. Next, you’ll make the app slightly more interesting by presenting notifications to the user when her location changes significantly.

Adding Local Notifications

Now you are going to make an addition to the locationManager:didUpdateLocations: method. If the application is currently in the background state, you’ll generate a local notification so the user can see when a location is updated while the app is not running. Listing 5-15 contains these changes.

Listing 5-15.  Updating the locationManager:didUpdateLocations: method to include notifications

- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations
{
    // Make sure this is a recent location event
    CLLocation *lastLocation = [locations lastObject];
    NSTimeInterval eventInterval = [lastLocation.timestamp timeIntervalSinceNow];
    if(abs(eventInterval) < 30.0)
    {
        // Make sure the event is accurate enough
        if (lastLocation.horizontalAccuracy >= 0 &&
            lastLocation.horizontalAccuracy < 20)
        {
            self.locationInformationLabel.text = lastLocation.description;

            UILocalNotification *notification = [[UILocalNotification alloc] init];
            notification.alertBody =
              [NSString stringWithFormat:@"New Location: %.3f, %.3f",
               lastLocation.coordinate.latitude, lastLocation.coordinate.longitude];
            notification.alertAction = @"Ok";
            notification.soundName = UILocalNotificationDefaultSoundName;
            //Increment the applicationIconBadgeNumber
            notification.applicationIconBadgeNumber =
              [[UIApplication sharedApplication] applicationIconBadgeNumber] + 1;
            [[UIApplication sharedApplication]
              presentLocalNotificationNow: notification];
        }
    }
}

With this new app, you can receive local notifications for each significant location change, even while the application is not in the foreground. These changes are reflected in a notification badge on the app’s icon, as well as a normal device notification. Keep in mind that it might take a little while in your simulator for a significant change to happen, as shown in Figure 5-7.

9781430259596_Fig05-07.jpg

Figure 5-7. A significant change occurrence in the background

Recipe 5-3: Tracking Magnetic Bearing

Modern iPhones and iPads contain magnetometers, hardware that can be used to determine the direction in which the device is being held. The measurement is based on the device’s position in relation to the magnetic north pole of the earth.

The magnetic poles are not the same as the geographic poles of the earth. Magnetic north is located in Northern Canada and moves slowly by approximately 55–60 km per year toward the west as the earth’s core changes.

Despite being somewhat inaccurate, the magnetic bearing is good enough for most applications, and it’s much less expensive in terms of battery power. This recipe shows you how to implement tracking of the magnetic bearing of the device.

About Heading Tracking

Heading tracking gives us the ability to track a user’s direction relative to north in real time. Implementing heading tracking is very similar to implementing any of the location tracking services discussed so far: You will include the Core Location framework in your project, create a location manager object, and define its delegate methods.

It is assumed that the device heading is measured while in portrait mode with the top pointing away from the user. You can change this by setting the headingOrientation property on the CLLocationManager object.

The options for this property are as follows:

  • CLDeviceOrientationPortrait (default)
  • CLDeviceOrientationPortraitUpsideDown
  • CLDeviceOrientationLandscapeLeft
  • CLDeviceOrientationLandscapeRight

Setting Up the Application

Let’s start, as usual, by creating a new single view application project. You will build an application very similar to the preceding two recipes, so either copy one of those projects or create a new one based on the following steps. In case you’ve decided to make a copy of a previous project, we’ve marked the differences in bold.

  1. Link the application to the Core Location framework.
  2. Add a text view and a switch control to the main view, which should look something like Figure 5-2 in Recipe 5-1. The switch should initially be set to “Off.”
  3. Create outlets for the label and the switch, and name them “headingInformationView” and “headingUpdatesSwitch,” respectively.
  4. Create an action for the switch, name it “toggleHeadingUpdates,” and set the event type to “Value Change.”
  5. Import the Core Location framework API by adding the following declaration in your view controller’s header file: #import <CoreLocation/CoreLocation.h>.
  6. Make the view controller a location manager delegate by adding the CLLocationManagerDelegate protocol to the ViewController class.
  7. Finally, add a CLLocationManager * instance variable to the view controller and name it “_locationManager.”

Refer to Recipe 5-1 for the details regarding the preceding steps. Your view controller’s header class should now look something like Listing 5-16.

Listing 5-16.  The completed view controller header

//
//  ViewController.h
//  Recipe 5-3 Determining Magnetic Bearing
//

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

@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}

@property (strong, nonatomic) IBOutlet UITextView *headingInformationView;
@property (strong, nonatomic) IBOutlet UISwitch *headingUpdatesSwitch;

- (IBAction) toggleHeadingUpdates:(id)sender;

@end

Note   If you’ve copied the project, you need to change the names of the outlets and the action. Be sure to use the Rename Refactoring tool in Xcode to do the renaming for you. This way you don’t have to reconnect them in Interface Builder.

To bring up the Rename tool, select the outlet (or action) property name that you want to rename. Ctrl-click to bring up the context menu and select Refactor arrow.jpg Rename, as shown in Figure 5-8.

9781430259596_Fig05-08.jpg

Figure 5-8. Navigation to Refactor arrow.jpg Rename

Starting and Stopping Heading Updates

Switch to the view controller’s implementation file (ViewController.m) and scroll to the bottom to start defining the toggleHeadingUpdates method.

Not all iOS devices can deliver heading information. Therefore, when the user has turned the switch to “on,” check whether a heading is available. If it’s not, turn the switch back to “off” and inform the user by means of the label, as shown in Listing 5-17.

Listing 5-17.  Checking for availability of location services

- (IBAction)toggleHeadingUpdates:(id)sender
{
    if (self.headingUpdatesSwitch.on == YES)
    {
        // Heading data is not available on all devices
        if ([CLLocationManager headingAvailable] == NO)
        {
            self.headingInformationView.text = @"Heading services unavailable";
            self.headingUpdatesSwitch.on = NO;
            return;
        }

        // ...

Now initialize the location manager, as shown in Listing 5-18, if it hasn’t already been instantiated. When creating an instance of CLLocationManager that is going to track heading changes, you should specify the headingFilter property. This property specifies how far (in degrees) your heading has to change before your delegate method is called.

Listing 5-18.  Initializing the location manager with a tracking filter of 5 degrees

if (_locationManager == nil)
{
    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.headingFilter = 5; // degrees
    _locationManager.delegate = self;
}

Finally, start and stop heading updates with the startUpdatingHeading and stopUpdatingHeading methods. The complete toggleHeadingUpdates should look like Listing 5-19.

Listing 5-19.  The completed toggleheadingUpdates: method

- (IBAction)toggleHeadingUpdates:(id)sender
{
    if (self.headingUpdatesSwitch.on == YES)
    {
        // Heading data is not available on all devices
        if ([CLLocationManager headingAvailable] == NO)
        {
            self.headingInformationLabel.text = @"Heading services unavailable";
            self.headingUpdatesSwitch.on = NO;
            return;
        }

        if (_locationManager == nil)
        {
            _locationManager = [[CLLocationManager alloc] init];
            _locationManager.headingFilter = 5; // degrees
            _locationManager.delegate = self;
        }

        [_locationManager startUpdatingHeading];
        self.headingInformationView.text = @"Starting heading tracking...";
    }
    else
    {
        // Switch was turned off
        self.headingInformationView.text = @"Stopped heading tracking";
        // Stop updates if they have been started
        if (_locationManager != nil)
        {
            [_locationManager stopUpdatingHeading];
        }
    }
}

Implementing Delegate Methods

The delegate methods need to be defined next. With heading tracking services, three delegate methods are required:

  • locationManager:didFailWithError:
  • locationManager:didUpdateHeading:
  • locationManagerShouldDisplayHeadingCalibration:

The first method, didFailWithError, is the same delegate method you have implemented with the location tracking services discussed previously. However, the difference is that the user, unlike location services, cannot deny heading tracking. So if an error occurs, you simply log the error to the console and hope it’s temporary. This is adequate for your testing purposes, but in a real-use scenario you probably should find out what kind of errors might occur and take appropriate action. You might also want to read the recipe on default error handling in Chapter 1 for ideas on how you can generally approach errors. This implementation is given in Listing 5-20.

Listing 5-20.  Complete implementation of the locationManager:didFailWithError: message

-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    NSLog(@”Error while tracking heading: %@”, error);
}

The next method, didUpdateHeading, gets invoked when the change in the heading of the device exceeds your headingFilter property. As with location updates, first check to see whether the update is a recent reading. Also, make sure that the reading is valid by checking the headingAccuracy property, which will be negative if the heading is invalid. If the reading is both recent and valid, update the label with the value from the magneticHeading property, rounding off to one decimal place. Listing 5-21 shows the complete implementation.

Listing 5-21.  Complete implementation of the locationManager:didUpdateHeading: method

-(void)locationManager:(CLLocationManager *)manager
didUpdateHeading:(CLHeading *)newHeading
{
    NSTimeInterval headingInterval = [newHeading.timestamp timeIntervalSinceNow];
    // Check if reading is recent
    if(abs(headingInterval) < 30)
    {
        // Check if reading is valid
        if(newHeading.headingAccuracy < 0)
            return;

        self.headingInformationView.text =
          [NSString stringWithFormat:@"%.1f°", newHeading.magneticHeading];
    }
}

Tip   You can use Alt+Shift+8 to insert the degree (°) symbol.

The final delegate method you need to implement is locationManagerShouldDisplayHeadingCalibration. This method determines whether the heading calibration screen should be presented. This is the scene that prompts a user to move her device in a figure-eight pattern to calibrate the magnetometer. This is a rather helpful feature, so simply return YES, as shown in Listing 5-22.

Listing 5-22.  Implementation for showing the calibration screen

-(BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager
{
    return YES;
}  

The application is now finished. Unfortunately, the simulator doesn’t support heading simulation, so you’ll need to test it on an actual device. When run, it’ll look something like Figure 5-9. It displays the heading relative to the magnetic north pole. A value close to 0 or 360 means north, 90 means east, 180 south, and 240 west.

9781430259596_Fig05-09.jpg

Figure 5-9. The application displaying magnetic bearing

That concludes Recipe 5-3. In the next recipe you’ll extend this project to include true bearing tracking alongside the magnetic bearing.

Note   Like the other recipes in this chapter that make use of the magnetometer, this functionality works only on a physical device and not in the simulator.

Recipe 5-4: Tracking True Bearing

You have figured out how to get the magnetic north heading, but what about true north? The difference between magnetic north and true north is called declination. Declination can vary greatly depending on where you are on the planet, but if you know where you are you can calculate it. The Core Location framework does this for you and provides it in the trueHeading property of a CLHeading object. All you need to do is also call the startUpdatingLocation method on your location manager to get the true north heading.

Caution   In this recipe you extend the project in Recipe 5-3 to include true heading tracking along with the magnetic bearing. Thus, it might be a good idea to make a backup of that project before you start.

Adding True Bearing

Because you are using the location service again, start by adding the NSLocationUsageDescription key with a usage description to the info.plist, such as “Testing true bearing.” This is similar to what we did in Figure 5-2 except the description should now be “Testing true bearing.”

Next, add a second text view and title for displaying the true heading in the main view. Your user interface should now look something like Figure 5-10.

9781430259596_Fig05-10.jpg

Figure 5-10. New user interface with an added label

Create an outlet named trueHeadingInformationView for the new text view. Your view controller’s interface file (.h) should now look like Listing 5-23.

Listing 5-23.  The complete view controller header file

//
//  ViewController.h
//  Recipe 5-4: Tracking True Bearing
//

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

@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}

@property (strong, nonatomic) IBOutlet UITextView *headingInformationView;
@property (strong, nonatomic) IBOutlet UITextView *trueHeadingInformationView;
@property (strong, nonatomic) IBOutlet UISwitch *headingUpdatesSwitch;

- (IBAction)toggleHeadingUpdates:(id)sender;

@end

Only a few changes need to be made to your existing code. Let’s start with the toggleHeadingUpdates: method.

Because you will be using location services again, reintroduce the control code from Recipe 5-1 that makes sure location services are enabled. This is shown in Listing 5-24.

Listing 5-24.  Adding  location services control code from Listing 5-4

if (self.headingUpdatesSwitch.on == YES)
{
    // ...

    if ([CLLocationManager locationServicesEnabled] == NO)
    {
        UIAlertView *locationServicesDisabledAlert = [[UIAlertView alloc]
            initWithTitle:@"Location Services Disabled"
            message:@"This feature requires location services. Enable it in the privacy settings on your device"
            delegate:nil
            cancelButtonTitle:@"Dismiss"
            otherButtonTitles:nil];

        [locationServicesDisabledAlert show];
        self.headingUpdatesSwitch.on = NO;
        return;
    }

    // ...

Then, to get true heading readings you need to start the location services in addition to starting heading updates. Listing 5-25 shows the complete toggleHeadingUpdates: method with the changes in bold.

Listing 5-25.  The complete toggleHeadingUpdates: method

- (IBAction)toggleHeadingUpdates:(id)sender
{
    if (self.headingUpdatesSwitch.on == YES)
    {
        // Heading data is not available on all devices
        if ([CLLocationManager headingAvailable] == NO)
        {
            self.headingInformationView.text = @"Heading services unavailable";
            self.headingUpdatesSwitch.on = NO;
            return;
        }

        if ([CLLocationManager locationServicesEnabled] == NO)
        {
            UIAlertView *locationServicesDisabledAlert =
                [[UIAlertView alloc] initWithTitle:@"Location Services Disabled"
                message:@"This feature requires location services. Enable it in the privacy settings on your device"
                delegate:nil
                cancelButtonTitle:@"Dismiss"
                otherButtonTitles:nil];

            [locationServicesDisabledAlert show];
            self.headingUpdatesSwitch.on = NO;
            return;
        }

        if (_locationManager == nil)
        {
            _locationManager = [[CLLocationManager alloc] init];
            _locationManager.headingFilter = 5; // degrees
            _locationManager.delegate = self;

}
        [_locationManager startUpdatingHeading];
        // Start location service in order to get true heading
        [_locationManager startUpdatingLocation];
        self.headingInformationView.text = @"Starting heading tracking...";
    }
    else
    {
        // Switch was turned off
        self.headingInformationView.text = @"Stopped heading tracking";
        // Stop updates if they have been started
        if (_locationManager != nil)
        {
            [_locationManager stopUpdatingHeading];
            [_locationManager stopUpdatingLocation];
        }
    }
}

Also, insert the error handling code to the locationManager:didFailWithError: delegate method, as shown in Listing 5-26.

Listing 5-26.  Implementing the error handling code in the locationManager:didFailWithError: method

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    if (error.code == kCLErrorDenied)
    {
        // Turning the switch to off will indirectly stop
        // further updates from coming
        self.headingUpdatesSwitch.on = NO;
    }
    else
    {
        NSLog(@"%@", error);
    }
}

Now, in your locationManager:didUpdateHeading: method, add a new statement to check the trueHeading value, as shown in Listing 5-27. A negative trueHeading value indicates that it’s invalid, so you want to use the trueHeading property only if it is greater than or equal to 0.

Listing 5-27.  Adding test for invalid reading in locationManager:didUpdateHeading: method

-(void)locationManager:(CLLocationManager *)manager
didUpdateHeading:(CLHeading *)newHeading
{
    NSTimeInterval headingInterval = [newHeading.timestamp timeIntervalSinceNow];
    if(abs(headingInterval)<30)
    {
        if (newHeading.headingAccuracy < 0)
            return;

        self.headingInformationView.text =
           [NSString stringWithFormat:@" Magnetic Heading:%.1f°",
                newHeading.magneticHeading];

        if(newHeading.trueHeading >= 0)
            self.trueHeadingInformationView.text =
                [NSString stringWithFormat:@"True Heading: %.1f°",
                    newHeading.trueHeading];
    }
}

Upon testing this application, you can get a simple readout of your device’s true and magnetic headings. As you can see in Figure 5-11, the two values differ somewhat, more or less depending on your current location.

9781430259596_Fig05-11.jpg

Figure 5-11. Displaying both magnetic and true heading values

Recipe 5-5: Region Monitoring

Core Location provides a method for monitoring when a device enters or exits a circular region. This can be a very useful feature for an application; for example, it can trigger an alert when a device enters the vicinity of a certain location, like triggering an alert to pick up milk when you get near the grocery store. You could also use it to send a notification to your family when you leave work to let them know that you are on your way home. Many possibilities are available if you let your imagination do a little wandering.

A Thing or Two About Regions

Regions are defined by a center coordinate and a radius measured in meters. The monitoring method triggers an event only when you cross a region boundary. It will not trigger an event if the device exists in the region when the monitoring starts. Events are triggered only when a device enters or exits a region.

Once you create a CLLocationManager object, you can register multiple regions for monitoring using the startMonitoringForRegion: method. The regions that you register for monitoring are persistent across multiple launches of your application. If your application is not running when a boundary event occurs, your application is automatically relaunched in the background so that it can process the event. All the regions you set up previously are available in the monitoredRegions property of the CLLocationManager object.

Regions are shared system wide, and only a limited number of regions can be monitored at a given time. You should always limit the number of defined regions that you are currently monitoring so as not to consume the system resources. You should remove regions for monitoring that are not near the device’s current location. For instance, there is no need to monitor for regions in Maryland if the device is on the West Coast. The error kCLErrorRegionMonitoringFailurewill be presented to the locationManager:monitoringDidFailForRegion:withError: delegate method if space is unavailable when you try to register a new region for monitoring.

Welcome to Denver!

In this project, you will create a region for the city of Denver, Colorado, and welcome visitors to the city when they enter it. You will start by creating a new single view application.

You will follow the same pattern and build a user interface as in the previous recipes. Follow these steps to set up the application:

  1. Link the application to the Core Location framework.
  2. Set a usage description (such as “Testing region monitoring”) for the NSLocationUsageDescription key in the application’s property list.
  3. Add a label, a text view, and a switch control to the main view, which should look something like Figure 5-2 in Recipe 5-1. The switch should be initially set to “Off.”
  4. Create outlets for the text view and the switch. Name the text view and switch “regionInformationView” and “regionMonitoringSwitch,” respectively.
  5. Create an action for the switch; name it “toggleRegionMonitoring” and make sure the event type is set to “Value Change.”
  6. Import the core location framework API by adding the following declaration in your view controller’s header file: #import <CoreLocation/CoreLocation.h>.
  7. Make the view controller a location manager delegate by adding the CLLocationManagerDelegate protocol to the ViewController class.
  8. Finally, add a CLLocationManager * instance variable to the view controller and name it “_locationManager.”

Your view controller’s header file should now look like Listing 5-28.

Listing 5-28.  The completed view controller header file

//
//  ViewController.h
//  Recipe 5-5: Region Monitoring
//

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

@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}

@property (strong, nonatomic) IBOutlet UITextView *regionInformationView;
@property (strong, nonatomic) IBOutlet UISwitch *regionMonitoringSwitch;

- (IBAction)toggleRegionMonitoring:(id)sender;

@end

Switching to the implementation file (.m), you can implement your region tracking methods. Let’s start with the toggleRegionMonitoring: method, as shown in Listing 5-29. If the switch is turned on, you should check whether region monitoring is available and enabled by the user before you start the monitoring. Note that you check whether monitoring is enabled by using the authorizationStatus class method, which simply returns a Boolean value if the status is authorized or not determined. If the status is kCLAuthorizationStatusNotDetermined, the user will be prompted by the operating system and asked for permission to use the location services.

Listing 5-29.  Checking whether or not location monitoring is authorized

- (IBAction)toggleRegionMonitoring:(id)sender
{
    if (self.regionMonitoringSwitch.on == YES)
    {
            CLAuthorizationStatus status = [CLLocationManager authorizationStatus];

            if (status == kCLAuthorizationStatusAuthorized ||
                status == kCLAuthorizationStatusNotDetermined)
            {
                // Start monitoring here
            }
            else
            {
                self.regionInformationView.text = @"Region monitoring disabled";
                self.regionMonitoringSwitch.on = NO;
            }

        }
    }
}

In the same method, within the “if” statement that checks the authorization status, you will need to instantiate your location manager instance variable if it is not already created. You’ll also need to set desiredAccuracy and delegate, as shown in Listing 5-30.

Listing 5-30.  Initializing a locationManager instance and setting a delegate and desiredAccuracy

if (status == kCLAuthorizationStatusAuthorized ||
    status == kCLAuthorizationStatusNotDetermined)
{
    if(_locationManager == nil)
    {
         _locationManager = [[CLLocationManager alloc] init];
         _locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
         _locationManager.delegate = self;
    }

    // ...

You need to define the center coordinate of the region you want to monitor as well as the radius of the region. Be careful when specifying the radius because if it is too large, the monitoring will fail. You can check to make sure your radius is within the radius bounds by comparing it to the maximumRegionMonitoringDistance property of the CLLocationManager object.

Once you have the center coordinate and radius, create the CLCircularRegion object right after the “if” statement, if(_locationManager ==), and provide it with an identifier for future reference, as shown in Listing 5-31.

Listing 5-31.  Creating a CLCircularRegion using a coordinate and a region radius

if (status == kCLAuthorizationStatusAuthorized ||
    status == kCLAuthorizationStatusNotDetermined)
{
    if(_locationManager == nil)
    {
         _locationManager = [[CLLocationManager alloc] init];
         _locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
         _locationManager.delegate = self;
    }

    CLLocationCoordinate2D denverCoordinate =
    CLLocationCoordinate2DMake(39.7392, -104.9847);
    int regionRadius = 3000; // meters
    if (regionRadius > _locationManager.maximumRegionMonitoringDistance)
    {
        regionRadius = _locationManager.maximumRegionMonitoringDistance;
    }
          CLCircularRegion *denverRegion = [[CLCircularRegion alloc] initWithCenter:denverCoordinate

radius:regionRadius

identifier:@"denverRegion"];
// ...

Once the region has been created, you can start monitoring for boundary events of that region by calling the startMonitoringForRegion: method of your location manager immediately after the last code segment, as shown in Listing 5-32.

Listing 5-32.  Line of code added to toggleRegionMonitoring to start monitoring

[_locationManager startMonitoringForRegion: denverRegion];

One last task you should do is turn off region monitoring if the user slides the switch to the “Off” position. To do this, access the monitoredRegions property of your location manager and turn off region monitoring for all the currently monitored regions, as shown in Listing 5-33. You could also choose to selectively turn off specific regions by utilizing the identifier property of the CLCircularRegion.

Listing 5-33.  Code added to toggleRegionMonitoring to turn off monitored regions

if (self.regionMonitoringSwitch.on == YES)
{
    // ...
}
else
{
    if (_locationManager!=nil)
    {
        for (CLCircularRegion *monitoredRegion in [_locationManager monitoredRegions])
        {
            [_locationManager stopMonitoringForRegion:monitoredRegion];
            self.regionInformationView.Text =
                [NSString stringWithFormat:@"Turned off region monitoring for: %@",
                     monitoredRegion.identifier];
        }
    }
}

The delegate methods need to be defined as well. There are two delegate methods for handling boundary events and one for handling errors:

  • locationManager:didEnterRegion:
  • locationManager:didExitRegion:
  • locationManager:monitoringDidFailForRegion:withError:

There are two main error codes that are related to region monitoring. One is kCLErrorRegionMonitoringDenied, which is used when the user of the device has specifically denied access to region monitoring. The other is kCLErrorRegionMonitoringFailure, which is used when monitoring for a specific region has failed, usually because the system has no more region resources available to the application. Add the code in Listing 5-34 to the end of the ViewController.m file.

Listing 5-34.  Implementing the locationManager:monitoringDidFailForRegion:withError delegate method

-(void)locationManager:(CLLocationManager *)manager
monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error
{
    switch (error.code)
    {
        case kCLErrorRegionMonitoringDenied:
        {
            self.regionInformationView.text =
                @"Region monitoring is denied on this device";
            break;
        }
        case kCLErrorRegionMonitoringFailure:
        {
            self.regionInformationView.text =
                [NSString stringWithFormat:@"Region monitoring failed for region: %@",
                    region.identifier];
            break;
        }
        default:
        {
            self.regionInformationView.text =
                [NSString stringWithFormat:@"An unhandled error occured: %@",
                     error.description];
            break;
        }
    }
}

locationManager:didEnterRegion: and locationManager:didExitRegion: can perform any function you want. Because the application could be in the background when the boundary event occurs, you will use local notifications in addition to updating the label to let the user know the event occurred, as shown in Listing 5-35.

Listing 5-35.  Adding the delegate implementations to detect entering and exiting a region

-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
    self.regionInformationView.text = @"Welcome to Denver!";

    UILocalNotification *entranceNotification = [[UILocalNotification alloc] init];
    entranceNotification.alertBody = @"Welcome to Denver!";
    entranceNotification.alertAction = @"Ok";
    entranceNotification.soundName = UILocalNotificationDefaultSoundName;
    [[UIApplication sharedApplication]
          presentLocalNotificationNow: entranceNotification];
}

-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
    self.regionInformationView.text =
        @"Thanks for visiting Denver! Come back soon!";

    UILocalNotification *exitNotification = [[UILocalNotification alloc] init];
    exitNotification.alertBody=@"Thanks for visiting Denver! Come back soon!";
    exitNotification.alertAction=@"Ok";
    exitNotification.soundName = UILocalNotificationDefaultSoundName;
    [[UIApplication sharedApplication]
         presentLocalNotificationNow:exitNotification];
}

To test this functionality using the iOS simulator, you must be able to feed in custom coordinates to be simulated. Like the freeway simulation in previous recipes, you can enter custom coordinates by navigating to Debug arrow.jpg Location arrow.jpg Custom Location, from which you can enter your own coordinates to test with. As an example, you could try latitude 39.7392 and longitude -104.9847, which should bring you inside the Denver region and make your app respond with the welcoming message. Then change the latitude to 39.0 (same longitude as before) and see your app welcome you back. You might also want to try putting the app into the background and switching the locations to verify the notifications pop-up.

Recipe 5-6: Implementing Geocoding

Location coordinates are useful to applications, but they are not very friendly to human beings. When is the last time you wrote your address using latitude and longitude coordinates? It’s just not human-friendly. Human locations are expressed in names that reference countries, states, cities, and so on. So when a device’s user asks, “Where am I?,” the user doesn’t want to know the GPS coordinates—the user wants to know what town or city he is in.

Fortunately, Apple has provided a method called reverse geocoding that converts location coordinates into a human-readable format. This feature used to be provided by the Map Kit framework, but it has been incorporated into the Core Location framework since iOS 5.

Geocoding, whether forward or reverse, is performed using the CLGeocoder class. You instantiate a CLGeocoder object and then pass it a coordinate and a block of code to perform once it has performed the geocoding. This is a little different than the other location recipes discussed thus far, which used delegate methods.

Note   A device must have network access to perform geocoding requests.

Implementing Reverse Geocoding

Let’s create a new single view application. To set up the project, take the following steps:

  1. Link the application to the Core Location framework.
  2. Set a usage description (such as “Testing geocoding”) for the NSLocationUsageDescription key in the application’s property list.
  3. Add a text view and a button to the main view, which should look something like Figure 5-12. The TextView should contain about five lines (don’t forget to set the Lines property in the attributes inspector).
  4. Create outlets for the label and the button and name them “geocodingResultsView” and “reverseGeocodingButton,” respectively.
  5. Create an action for the switch, name it “findCurrentAddress,” and make sure the event type is set to “Value Change.”
  6. Import the Core Location framework API by adding the following declaration in your view controller’s header file: #import <CoreLocation/CoreLocation.h>.
  7. Make the view controller a location manager delegate by adding the CLLocationManagerDelegate protocol to the ViewController class.
  8. Add a CLLocationManager * instance variable to the view controller and name it “_locationManager.”
  9. Add a second instance variable, this time of the CLGeocoder * type and with the name “_geocoder.”

9781430259596_Fig05-12.jpg

Figure 5-12. Initial user interface for reverse geocoding

Your view controller’s header file should now look like Listing 5-36.

Listing 5-36.  The completed view controller header file

//
//  ViewController.h
//  Recipe 5-6 Implementing Geocoding
//

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

@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
    CLGeocoder *_geocoder;
}

@property (strong, nonatomic) IBOutlet UITextView *geocodingResultsView;
@property (strong, nonatomic) IBOutlet UIButton *reverseGeocodingButton;
- (IBAction)findCurrentAddress:(id)sender;

@end

Switch to the implementation file (ViewController.m), and scroll to the bottom to implement the method findCurrentAddress. Because this has been covered in preceding recipes in this chapter, we’re not going to go into detail about this, but we will cover some highlights.

You should follow the best practices of geocoding and not geocode a location that is too near to one you have already geocoded or that is too recent, so you will set your distanceFilter property on your CLLocationManager object to 500 meters.

In Listing 5-37, you set your desired accuracy to the constant kCLLocationAccuracyHundredMeters so that you get a faster response from the location tracking services and limit the drain on the battery.

Listing 5-37.  Implementing the findCurrentAddress action method

- (IBAction)findCurrentAddress:(id)sender
{
    if([CLLocationManager locationServicesEnabled])
    {
        if(_locationManager==nil)
        {
            _locationManager=[[CLLocationManager alloc] init];
            _locationManager.distanceFilter = 500;
            _locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
            _locationManager.delegate = self;

        }

        [_locationManager startUpdatingLocation];
        self.geocodingResultsView.text = @"Getting location...";
    }
    else
    {
        self.geocodingResultsView.text=@"Location services are unavailable";
    }
}

Now add your delegate methods for the CLLocationManager object. The first is the locationManager:didFailWithError method, as shown in Listing 5-38.

Listing 5-38.  Implementing the locationManager:didFailWithError: method

-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    if(error.code == kCLErrorDenied)
    {
        self.geocodingResultsView.text = @"Location information denied";
    }
}

Next the locationManager:didUpdateToLocations: delegate method has to be defined. Start with the standard checks to make sure the newLocation timestamp property is recent and that it is valid, as shown in Listing 5-39.

Listing 5-39.  The starting implementation of the location: didUpdateLocations: method

- (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;

        // ...
    }
}

Next check whether the _geocoder instance variable has been instantiated and, if not, create it. Also, make sure that you stop any existing geocoding services before performing a new one, as shown in Listing 5-40.

Listing 5-40.  Checking the geocoder instance variable before creation and stopping any existing services

- (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 (_geocoder == nil)
            _geocoder = [[CLGeocoder alloc] init];

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

Finally, start your reverse geocoding process and define the completion handler. The completion handler receives two objects: an array of placemarks and an error object. The placemarks consist of street, city, and so on. If the array contains one or more objects, then the reverse geocoding was successful. If not, then you can check the error code for details.

The resulting location:didUpdateToLocations: method is shown in Listing 5-41.

Listing 5-41.  The completed locationManager:didUpdateLocations: method

- (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 (_geocoder == nil)
            _geocoder = [[CLGeocoder alloc] init];

        //Only one geocoding instance per action
        //so stop any previous geocoding actions before starting this one
        if([_geocoder isGeocoding])
            [_geocoder cancelGeocode];

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

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

You now have an application that can do reverse geocoding and find the address of the current location. Build and run it to verify it works correctly before we move on and extend the functionality of the app with forward geocoding.

Implementing Forward Geocoding

In iOS 5, forward geocoding was introduced. This means that you can pass an address to a geocoder and receive the coordinates for that address. The more information you can provide about an address, the more accurate the resulting forward geocode will be.

Let’s extend the application with a feature that translates a given address into coordinates. Start by adding a text field and another button to the user interface. It should resemble Figure 5-13.

9781430259596_Fig05-13.jpg

Figure 5-13. The updated user interface with forward geocoding

Now, add an outlet called “addressTextField” for the text field. Next, add an action for the button and name it “findCoordinateOfAddress.”

In the findCoordinateOfAddress: action, the plan is to take the text the user has entered into the text field and send it to your geocoder object for translation into coordinates. The geocode process may result in multiple matches of possible coordinates, but the best guess is always first. Listing 5-42 shows the implementation of the method.

Listing 5-42.  Implementation of the findCoordinateOfAddress: action method

- (IBAction)findCoordinateOfAddress:(id)sender
{
    // Instantiate _geocoder if it has not been already
    if (_geocoder == nil)
        _geocoder = [[CLGeocoder alloc] init];

    NSString *address = self.addressTextField.text;
    [_geocoder geocodeAddressString:address
        completionHandler:^(NSArray *placemarks, NSError *error)
        {
            if ([placemarks count] > 0)
            {
                CLPlacemark *placemark = [placemarks objectAtIndex:0];

                self.geocodingResultsView.text = placemark.location.description;
            }
            else
            {
                self.geocodingResultsView.text = error.localizedDescription;
            }
        }
     ];
}

In case of an error, the implementation in Listing 5-43 outputs the error message to the label. There are, however, a couple of errors your code should expect and handle. These include network errors, core location denied by user errors, and errors when no geocoding results are found. Listing 5-43 is an updated implementation that extracts these errors and provides (slightly) better error messages in these cases.

Listing 5-43.  Adding error handling to the findCoordinateOfAddress: action method

- (IBAction)findCoordinateOfAddress:(id)sender
{
    // Instantiate _geocoder if it has not been already
    if (_geocoder == nil)
        _geocoder = [[CLGeocoder alloc] init];

    NSString *address = self.addressTextField.text;
    [_geocoder geocodeAddressString:address
        completionHandler:^(NSArray *placemarks, NSError *error)
        {
            if ([placemarks count] > 0)
            {
                CLPlacemark *placemark = [placemarks objectAtIndex:0];

                self.geocodingResultsView.text = placemark.location.description;
            }
            else if (error.domain == kCLErrorDomain)
            {
                switch (error.code)
                {
                    case kCLErrorDenied:
                        self.geocodingResultsView.text
                            = @"Location Services Denied by User";
                        break;
                    case kCLErrorNetwork:
                        self.geocodingResultsView.text = @"No Network";
                        break;
                    case kCLErrorGeocodeFoundNoResult:
                        self.geocodingResultsView.text = @"No Result Found";
                        break;
                    default:
                        self.geocodingResultsView.text = error.localizedDescription;
                        break;
                }
            }
            else
            {
                self.geocodingResultsView.text = error.localizedDescription;
            }

        }
     ];
}

That concludes Recipe 5-6. Let’s end with some best practices advice for geocoding.

Best Practices

Here are some best practices to be aware of when using reverse or forward geocoding:

  • You should send only one geocoding request at a time.
  • If the user performs an action that will result in the same location being geocoded, the results should be reused rather than requesting the same location multiple times.
  • You should not send more than one geocoding request per minute. You should check to see whether the user has moved a significant distance before calling another geocoding request.
  • Do not perform a geocoding request if you will not see the results (in other words, if your application is running in the background).

Summary

The Core Location framework is a powerful framework that can be utilized by any number of application features. As demonstrated in this chapter, you can determine where a device is located, which direction a device is facing, and when a device enters or exits a specific region. Beyond those powerful features, you can also perform lookups on geographical coordinates to determine human-readable location information that will be presented to your end user, as well as provide complementary services to perform the reverse.

Apple has walked a fine line of making powerful features available to developers while also respecting a user’s privacy and the battery drain on a device. As developers, we should work to deliver exciting features and functionality in our applications while maintaining the same level of respect for our users.

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

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