Chapter    4

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 use cases 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 6 offers better accuracy and better availability than its predecessor, as well as some new default behaviors that will improve battery life. With iOS 6 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

  • Location tracking
  • Monitoring significant location changes
  • Monitoring entrance 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; 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.

What’s New in iOS 6

The biggest change in the Core Location framework in iOS 6 has been on the inside. Still, there are a couple of changes in the API that are worth notingfor example, how to control the new autopause behavior of the location services and the deprecation of the Purpose property.

Autopause

Receiving location updates with high accuracy is a relatively expensive task in terms of battery power. You should therefore minimize the number of updates that are not needed. To help, Apple has implemented a new behavior that pauses updates automatically if the app is in background mode and receives updates that seem unnecessaryfor example, if during several updates the device has stopped moving, or desired accuracy can’t be provided.

When paused, the locationManagerDidPauseLocationUpdates: delegate method notifies the app. When the app is brought back to the foreground again, the location updates are resumed and you’ll get a locationManagerDidResumeLocationUpdates: message.

This behavior is turned on by default, but your app can turn it off with the pausesLocationUpdatesAutomatically property. Most apps, however, don’t need to care about autopausing. It probably works just like the user would expect and apps should take great care to minimize location updates anyway.

Activity-Type Property

To help the Core Location framework make better guesses as to what updates may be beneficial to the user, you should set the new property activityType. Currently, four values are available:

  • CLActivityTypeFitness, for movement on foot or by bicycle.
  • CLActivityTypeAutomotiveNavigation, for movement by car or motorcycle.
  • CLActivityTypeOtherNavigation, for other vehicular navigation; e.g. by boat, by train, or by plane.
  • CLActivityTypeOther, for any other type of usage.

For example:

locationManager.activityType = CLActivityTypeFitness

Deprecated Functionality

The locationManager.purpose property has been deprecated in iOS 6. The new way of explaining to the user why your app is requesting location services is to add the NSLocationUsageDescription key in your application’s property list and put the purpose string there. (Chapter 1 contains a recipe on how to add keys to the property list.)

Also, the startMonitoringForReqion:desiredAccuracy: method of the location manager has been deprecated. Instead, you should use the locationManager:startMonitoringForRegion: method.

On the location manager delegate, the new locationManager:didUpdateLocations: method supersedes the locationManager:didUpdateToLocation:fromLocation: method, which has also been deprecated.

Requiring Location Service

If your app can’t work at all without location services, you should prevent it from being loaded on a device that does not have the proper support. This is done by adding one or more values to the Required device capabilities array in the app’s property list. Figure 4-1 shows an example in which we’ve added general location services to the list of required capabilities.

9781430245995_Fig04-01.jpg

Figure 4-1.  Making location services a required capability in the app’s Info.plist file

You can add a number of different values to the Required device capabilities array, three of which are used in regard to Core Location: location-services, gps, and magnetometer.

Recipe 4-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 off by creating a new single-view application and add the Core Location framework to the project. (See Chapter 1 for instructions on how to create the project and link the framework library.)

You are going to use a very simple user interface with a single label to display the location information, and a switch control to let the user turn location updates on and off. So bring up the interface builder by selecting the view controller .xib file in the project navigator. Add the label and the switch to your user interface by dragging them from the object library. Make the label big enough to contain five rows; also be sure to set the label’s Lines property to 5 in the Attributes Inspector tab of the utilities pane. Likewise, make sure the initial state of the switch is set to “Off.”

Your user interface should now resemble the one in Figure 4-2.

9781430245995_Fig04-02.jpg

Figure 4-2.  User interface for Recipe 4-1

Now create outlet properties for the label and the switch. Name them locationInformationLabel 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 on 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.

// ...
#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 the following code).

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

// ...
@end

Your view controller’s header file should now look something like this:

//
//  ViewController.h
//  Recipe 4.1: Basic Location Information
//
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}
@property (strong, nonatomic) IBOutlet UILabel *locationInformationLabel;
@property (strong, 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 testingstandard location services" (without the quotes), as shown in Figure 4-3. When the user is prompted to allow your application access to his or her location, that text is displayed, telling the user what you plan to do with his or her device’s location information.

9781430245995_Fig04-03.jpg

Figure 4-3.  Setting 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’re going to 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 first want to check whether location services are enabled. If they are not, display an alert and turn the switch back to off.

- (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” is enabled, go on and 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. A number of constants are available to use for your convenience:

  • 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 (via 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 Core Location framework to better figure out when it should autopause location updates. For a list of possible values, refer to the list in the section “Activity-Type Properties.”

Once you've set the properties and the delegate in the ViewController.m file, you can start the location services by calling the startUpdatingLocation methodon your CLLocationManager.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
  // ...
The next step is not strictly necessary, but if you care about compatibility with pre-iOS 6 versions, you should also set the deprecated purpose property of the location manager. Because you have already provided the usage description in Info.plist (which is the new way of doing this), just grab the text from there.if (_locationManager == nil){

    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    _locationManager.distanceFilter = 1; // meter
    _locationManager.activityType = CLActivityTypeOther;
    _locationManager.delegate = self;

    // For backward compatibility, set the deprecated purpose property
    // to the same as NSLocationUsageDescription in the Info.plist
    _locationManager.purpose = [[NSBundle mainBundle]
        objectForInfoDictionaryKey:@"NSLocationUsageDescription"];

}
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.- (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 (i.e., 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 in to the console. Your code should look something like this:

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

- (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 want to check whether the timestamp of the location object is recent. Core Location has a habit of presenting 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 you filter out location events that are more than 30 seconds old:

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

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

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

    }
}

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 locationInfomationLabel text to the lastLocation.description value, resulting in the previous 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. Like Figure 4-4 shows, there are functions for setting a custom location, or simulating different scenarios such as a city run or a freeway drive.

9781430245995_Fig04-04.jpg

Figure 4-4.  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 4-5 that the string you set in the application’s property list is displayed.

9781430245995_Fig04-05.jpg

Figure 4-5.  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 image Location image Freeway Drive, and the label should start to update with information about the prerecorded drive that Apple has provided. Figure 4-6 shows a sample of information delivered by the simulated drive.

9781430245995_Fig04-06.jpg

Figure 4-6.  Displaying simulated location information

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

Recipe 4-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 4-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 (e.g., "Testing the significant location change service") for the NSLocationUsageDescription Info.plist key ("PrivacyLocation Usage Description” in the property list).
  3. Add a label and a switch control to the main view (the .xib file), which should look something like Figure 4-2 in Recipe 4-1. The label should contain about five lines and the switch should be initially set to "Off."
  4. Create outlets for the label and the switch, name them locationInformationLabel 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, name it _locationManager.

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

//
//  ViewController.h
//  Recipe 4.2: Significant Location Change
//
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}
@property (strong, nonatomic) IBOutlet UILabel *locationInformationLabel;
@property (strong, nonatomic) IBOutlet UISwitch *locationUpdatesSwitch;
- (IBAction)toggleLocationUpdates:(id)sender;

@end

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

Enabling Background Updates

For this recipe you will enable location updates to come even when your app is residing in the background mode. To do this you need to add another key to theInfo.plist file. So go ahead and add the UIBackgroundModes key (or "Required background modes" as Xcode translates it to in the user interface). This is an array value and what you want to do is to add a sub item to it with the value App registers for location updates, as in Figure 4-7.

9781430245995_Fig04-07.jpg

Figure 4-7.  Specifying location changes as a required background mode

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 4-1. The only difference is that you use start and stopMonitoringSignificantLocationChanges instead of start and stopUpdatingLocation. Also, significant location change service doesn’t care about the desiredAccuracy, distanceFilter, and activityType properties, so you can leave them out.

Below is the newtoggleLocationUpdates: method, differences marked in bold. Go ahead and implement it in your project.

- (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;
            // For backward compatibility, set the deprecated purpose property
            // to the same as NSLocationUsageDescription in the Info.plist
            _locationManager.purpose = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationUsageDescription"];
        }
        [_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 previous recipe. Here locationManager:didFailWithError:

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

And locationManager:didUpdateLocations:

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

The app is nearly finished so you can build and test it now. Then you’ll make it slightly more interesting by presenting notifications to the user when his or 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 that the user can see when a location is updated while the app is not running.

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

Recipe 4-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

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 and 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 are going to build an application very similar to the previous 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 label and a switch control to the main view (the .xib file), which should look something like Figure 4-2 in Recipe 4-1. The switch should be initially set to “Off.”
  3. Create outlets for the label and the switch, name them headingInformationLabel 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, name it _locationManager.

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

//
//  ViewController.h
//  Recipe 4.3: Determining Magnetic Bearing
//
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
@interface ViewController : UIViewController<CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}
@property (strong, nonatomic) IBOutlet UILabel *headingInformationLabel;
@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|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 heading is available. If it’s not, turn the switch back to off and inform the user via the label.

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

        // ...

Now initialize the location manager, 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.

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 something like this:

- (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.headingInformationLabel.text = @"Starting heading tracking...";
    }
    else
    {
        // Switch was turned Off
        self.headingInformationLabel.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 need to be defined:

  • 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 want to find out what kind of errors might occur and take appropriate action. You may also want to read the recipe on default error handling in Chapter 1 for ideas on how you can generally approach errors.

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

The next method, didUpdateHeading, gets invoked when the change in 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 themagneticHeading property, rounding off to one decimal.

-(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.headingInformationLabel.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 his or her device in a figure-eight pattern to calibrate the magnetometer (see Figure 4-8). This is a rather helpful feature, so  simply return YES:

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

}

9781430245995_Fig04-08.jpg

Figure 4-8.  Heading calibration message

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 4-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.

9781430245995_Fig04-09.jpg

Figure 4-9.  The application displaying magnetic bearing

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

Recipe 4-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. 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.

In this recipe you extend the project in Recipe 4-3 to include true heading tracking along with the magnetic bearing. So it may 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, for example “Testing true bearing.”

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

9781430245995_Fig04-10.jpg

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

Create an outlet named trueHeadingInformationLabel for the new label. Your view controller’s interface file (.h) should now look like the following block:

//
//  ViewController.h
//  Recipe 4.4: Determining True Bearing
//
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
@interface ViewController : UIViewController <CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}
@property (strong, nonatomic) IBOutlet UILabel *headingInformationLabel;
@property (strong, nonatomic) IBOutlet UILabel *trueHeadingInformationLabel;
@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 4-1 that makes sure location services are enabled.

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, in order to get true heading readings you need to start the location services in addition to starting heading updates. You may also want to set the deprecated purpose property of the location manager. Here’s the completetoggleHeadingUpdates: method with the changes in bold.

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

            // For backward compatibility, set the deprecated purpose property
            // to the same as NSLocationUsageDescription in the Info.plist
            _locationManager.purpose = [[NSBundle mainBundle]
                objectForInfoDictionaryKey:@"NSLocationUsageDescription"];


        }
        [_locationManager startUpdatingHeading];
        // Start location service in order to get true heading
        [_locationManager startUpdatingLocation];

        self.headingInformationLabel.text = @"Starting heading tracking...";
    }
    else
    {
        // Switch was turned Off
        self.headingInformationLabel.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.

- (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 didUpdateHeading: method, add a new statement to check the trueHeading value. 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:

-(void)locationManager:(CLLocationManager *)manager
didUpdateHeading:(CLHeading *)newHeading
{
    NSTimeInterval headingInterval = [newHeading.timestamp timeIntervalSinceNow];
    if(abs(headingInterval) < 30)
    {
        if (newHeading.headingAccuracy < 0)
            return;
        self.headingInformationLabel.text =
           [NSString stringWithFormat:@"Magnetic Heading: %.1f°",
                newHeading.magneticHeading];

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

    }
}

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

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

9781430245995_Fig04-11.jpg

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

Recipe 4-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 Baltimore!

In this project, you are going to create a region for the city of Baltimore, Maryland, and welcome visitors to the city when they enter it. You start by creating a new single-view application.

You are going to follow the same pattern and build a user interface much like 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 (e.g., “Testing region monitoring”) for the NSLocationUsageDescription key in the application’s property list
  3. Add a label and a switch control to the main view (the .xib file), which should look something like Figure 4-2 in Recipe 4-1. The switch should be initially set to “Off.
  4. Create outlets for the label and the switch, name them regionInformationLabel and regionMonitoringSwitch.
  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, name it _locationManager.

Your view controller’s header file should now look like the following:

//
//  ViewController.h
//  Recipe 4.5: Welcome to Baltimore
//
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
@interface ViewController : UIViewController < CLLocationManagerDelegate>
{
    CLLocationManager *_locationManager;
}
@property (strong, nonatomic) IBOutlet UILabel *regionInformationLabel;
@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. 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. If the status iskCLAuthorizationStatusNotDetermined, the user will be prompted by the operating system and asked for permission to use the location services.

- (IBAction)toggleRegionMonitoring:(id)sender
{
    if (regionMonitoringSwitch.on == YES)
    {
        if([CLLocationManager regionMonitoringAvailable])
        {
            CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
            if (status == kCLAuthorizationStatusAuthorized ||
                status == kCLAuthorizationStatusNotDetermined)
            {
                // Start monitoring here
            }
            else
            {
                self.regionInformationLabel.text = @"Region monitoring disabled";
                regionMonitoringSwitch.on = NO;
            }
        }
        else
        {
            self.regionInformationLabel.text = @"Region monitoring not available";
            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, delegate, and (for backward compatibility with pre-iOS 6 versions ) the purpose property.

if (status == kCLAuthorizationStatusAuthorized ||
    status == kCLAuthorizationStatusNotDetermined)
{
    if(_locationManager == nil)
    {
         _locationManager = [[CLLocationManager alloc] init];
         _locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
         // For backward compatibility, set the deprecated purpose property
         // to the same as NSLocationUsageDescription in the Info.plist
         _locationManager.purpose = [[NSBundle mainBundle]
             objectForInfoDictionaryKey:@"NSLocationUsageDescription"];
         _locationManager.delegate = self;
    }


    // ...

You need to define the center coordinate of the region you want to monitor and 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, you create the CLRegion object and provide it with an identifier for future reference:

CLLocationCoordinate2D baltimoreCoordinate =
    CLLocationCoordinate2DMake(39.2963, −76.613);
int regionRadius = 3000; // meters
if (regionRadius > _locationManager.maximumRegionMonitoringDistance)
{
    regionRadius = _locationManager.maximumRegionMonitoringDistance;
}
CLRegion *baltimoreRegion = [[CLRegion alloc]
                             initCircularRegionWithCenter: baltimoreCoordinate
                             radius: regionRadius
                             identifier: @"baltimoreRegion"];

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.

 [_locationManager startMonitoringForRegion: baltimoreRegion];

One last thing you want to 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. You could also choose to selectively turn off specific regions by utilizing the identifier property of the CLRegion.

if (regionMonitoringSwitch.on == YES)
{
    // ...
}
else
{
    if (_locationManager != nil)
    {
        for (CLRegion *monitoredRegion in [_locationManager monitoredRegions])
        {
            [_locationManager stopMonitoringForRegion:monitoredRegion];
            self.regionInformationLabel.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, and it is used when the user of the device has specifically denied access to region monitoring. The other is kCLErrorRegionMonitoringFailure, and it is used when monitoring for a specific region has failed, usually because the system has no more region resources available to the application.

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

locationManager:didEnterRegion: and locationManager:didExitRegion: can perform any function that you want, and 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:

-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
    self.regionInformationLabel.text = @"Welcome to Baltimore!";
    UILocalNotification *entranceNotification = [[UILocalNotification alloc] init];
    entranceNotification.alertBody = @"Welcome to Baltimore!";
    entranceNotification.alertAction = @"Ok";
    entranceNotification.soundName = UILocalNotificationDefaultSoundName;
    [[UIApplication sharedApplication]
          presentLocalNotificationNow: entranceNotification];
}
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
    self.regionInformationLabel.text =
        @"Thanks for visiting Baltimore! Come back soon!";
    UILocalNotification *exitNotification = [[UILocalNotification alloc] init];
    exitNotification.alertBody = @"Thanks for visiting Baltimore! 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 image Location image Custom Location . . ., from which you can enter your own coordinates to test with. As an example, you could try latitude 39.3 and longitude −76.6, which should bring you inside the Baltimore 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.

Recipe 4-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 or she 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.

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

Implementing Reverse Geocoding

Geocoding 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 that used delegate methods.

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 (e.g., “Testing geocoding”) for the NSLocationUsageDescription key in the application’s property list.
  3. Add a label and a button to the main view (the .xib file), which should look something like Figure 4-12. The label should contain about five lines (don’t forget to set the Lines property in the attributes inspector).

    9781430245995_Fig04-12.jpg

    Figure 4-12.  Initial user interface for reverse geocoding

  4. Create outlets for the label and the button, name them geocodingResultsLabel and reverseGeocodingButton.
  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, name it _locationManager.
  9. Add a second instance variable, this time of the CLGeocoder * type and with the name _geocoder.

Your view controller’s header file should now look like the following:

//
//  ViewController.h
//  Recipe 4.6: Geocoding
//

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

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

@property (strong, nonatomic) IBOutlet UILabel *geocodingResultsLabel;
@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 previous recipes in this chapter, we’re not going to go into detail about this, but we will cover some highlights.

You want to 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’re going to set your distanceFilter property on your CLLocationManager object to 500 meters.

You are also going to 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.

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

            // For backward compatibility, set the deprecated purpose property
            // to the same as NSLocationUsageDescription in the Info.plist
            _locationManager.purpose = [[NSBundle mainBundle]
                  objectForInfoDictionaryKey: @"NSLocationUsageDescription"];
          }
        [_locationManager startUpdatingLocation];
        self.geocodingResultsLabel.text = @"Getting location...";
    }
    else
    {
        self.geocodingResultsLabel.text = @"Location services are unavailable";
    }
}

Now add your delegate methods for the CLLocationManager object. The first is the locationManager:didFailWithError method:

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

Next is the locationManager:didUpdateToLocations: delegate method to be defined. Start with the standard checks to make sure the newLocation timestamp property is recent and that it is valid:

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

- (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. 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 as follows:

- (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.geocodingResultsLabel.text =
                        [NSString stringWithFormat:@"You are in: %@",
                            foundPlacemark.description];
                }
                else if (error.code == kCLErrorGeocodeCanceled)
                {
                    NSLog(@"Geocoding cancelled");
                }
                else if (error.code == kCLErrorGeocodeFoundNoResult)
                {
                    self.geocodingResultsLabel.text = @"No geocode result found";
                }
                else if (error.code == kCLErrorGeocodeFoundPartialResult)
                {
                    self.geocodingResultsLabel.text = @"Partial geocode result";
                }
                else
                {
                    self.geocodingResultsLabel.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 before moving on and extending the application with some forward geocoding.

Implementing Forward Geocoding

In iOS 5, forward geocoding was introduced as well. 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 4-13.

9781430245995_Fig04-13.jpg

Figure 4-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. Here’s the implementation of the 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.geocodingResultsLabel.text = placemark.location.description;
            }
            else
            {
                self.geocodingResultsLabel.text = error.localizedDescription;
            }
        }
     ];
}

In case of an error, the foregoing implementation 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. Following is an updated implementation that extracts these errors and provides (slightly) better error messages in these cases:

- (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.geocodingResultsLabel.text = placemark.location.description;
            }
            else if (error.domain == kCLErrorDomain)
            {
                switch (error.code)
                {
                    case kCLErrorDenied:
                        self.geocodingResultsLabel.text
                            = @"Location Services Denied by User";
                        break;
                    case kCLErrorNetwork:
                        self.geocodingResultsLabel.text = @"No Network";
                        break;
                    case kCLErrorGeocodeFoundNoResult:
                        self.geocodingResultsLabel.text = @"No Result Found";
                        break;
                    default:
                        self.geocodingResultsLabel.text = error.localizedDescription;
                        break;
                }
            }

            else
            {
                self.geocodingResultsLabel.text = error.localizedDescription;
            }
        }
     ];
}

That concludes Recipe 4-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 (e.g., 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 to 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