Chapter 4

Location Recipes

The Core Location framework provides a new way to provide information to applications that is relevant to where the device is geographically located. With the features of this framework, your application can accurately tell where a device is located and even what direction it is facing. There is a variety of applications that have successfully seized opportunities to utilize location-aware information, such as Facebook and Foursquare. iOS 5 continues to improve on the possibilities available and provides new features to convert human-readable locations into geographical locations.

There are three main capabilities that we will deal with in this chapter: location services, GPS, and the magnetometer. Location services are the bare essential functionality that allows your application to access the location of the user. On top of this, by using assisted GPS, you can greatly improve your location accuracy (often at the price of battery life). The magnetometer is included on certain newer devices, which allows an application access to both the heading and bearing of a device.

Supported Devices

The first thing you need to consider when you are planning to incorporate location-based services into your application is what devices will support those services. Not all Apple devices are created equal, or rather not all of them include the capability to support location services. Specifically, none of the iPod Touches includes a GPS, and they can provide the device's location only based on a WiFi connection (if available and connected). Refer to Table 4–1 to see the supported location capabilities of current Apple devices.

Image

Requiring Location Services

If your application is completely dependent on location services, you may want to prevent it from being loaded on a device that does not support location services. You can require GPS, magnetometer, or location services in general. Place these requirements only if these capabilities are absolutely critical to the functionality of your application. To configure these requirements, click the project in the navigator pane, and select your project target in the editor window.

If you wish to add these requirements to an existing project, select the Info tab and add a row to the Custom iOS Target Properties listing. You can add a row by clicking the small “+” icon to the right of the key name, or to the right of the name of any existing rows. The key you want to add is “Required device capabilities” or UIRequiredDeviceCapabilities. There is no difference between these two names, as the latter will automatically be replaced by the former. This key contains an array of values that reference device capabilities that are required for a device to run your application. Expand the key, and add the required capabilities as items in the key, as highlighted in Figure 4–1.

Image

Figure 4–1. Specifying required device capabilities for an application

If you require just location services (i.e., knowing only the general location of a user without heading or GPS accuracy), adding the location services item alone is enough. If, however, your application requires GPS accuracy, then you may want to add the GPS requirement. If you include the GPS item, you should also include the location services item. And finally, if your device needs to know the heading of the device, you can include the magnetometer as a required capability.

How Do I Know Where I Am?

There are two primary methods for finding the location of a device: standard location service and significant location change service. Which one you use will depend 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 will invoke the GPS if the accuracy requested requires it. This greater accuracy comes at a cost in terms of a longer time to get an accurate 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. We'll discuss some best practices and techniques for using this service later in this chapter.

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 only the town or city that someone is in, the significant location change service is perfectly acceptable. You will get a fast response without using a lot of battery power because it uses the cellular signal to determine the device's location. Another additional benefit of the significant location change service is its ability to run in the background on the device. Your app does not have to be running in the foreground to receive location updates from this service.

These two services work in very similar ways. They require a CLLocationManager object to be instantiated that sets up the location services and specifies how it is to be used. The CLLocationManager object will also have a delegate defined. This delegate should respond to at least two methods:

- locationManager:didUpdateToLocation:fromLocation:
- locationManager:didFailWithError:

The implementation of these methods for each service will be discussed in the recipes that follow.

Recipe 4–1: Getting Device Location Information

To get the most accurate location information about a device, you are going to use the standard location service. Let's start by creating a new single view application called Chapter4SampleProject to implement this functionality. If your version of Xcode allows you to specify a class prefix, enter Chapter4SampleProject again.

The first thing you need to do when adding location services to an application is add the Core Location framework library to the application. The location of frameworks has changed slightly from Xcode 3 to Xcode 4. You will now find them when you select the project in the navigator pane and select your project's target. Switch over to the Build Phases tab, and expand the Link Binary With Libraries area to see the included frameworks, resembling Figure 4–2. Clicking the + button will allow you to add the Core Location framework to your project, as shown in Figure 4–3.

Image

Figure 4–2. Clicking the + button to add a framework

Image

Figure 4–3. Selecting CoreLocation.framework to add it to your project

Now you're going to set up your XIB to display the location information. Click the Chapter4SampleProject.xib file in the navigator pane, and Interface Builder will be loaded. You're going to drag a UILabel and UISwitch object onto the XIB. The UILabel will be used to display the location information, and the UISwitch will be used to turn location services on and off. You're going to set the initial state of the UISwitch to Off in the Attributes Inspector tab of the utilities pane. Your XIB now resembles Figure 4–4.

Image

Figure 4–4. User interface for displaying simple location information

Now create some outlet properties and actions. Turn on the Assistant Editor, and select the UIView in the XIB. ^-click-;drag from the UILabel to the interface file (.h) to create an outlet. In the pop-up that is displayed, name the UILabel “labelLocation”, as demonstrated in Figure 4–5.

Image

Figure 4–5. Connect your UILabel to an outlet

Repeat the same process with the UISwitch, but this time change the Connection type to Action and name the action “toggleLocationServices”, following Figure 4–6.

Image

Figure 4–6. Creating an action to be performed by the switch

Import the Core Location framework into the Chapter4SampleProjectViewController interface file (.h) by putting the following line at the top of the interface file:

#import <CoreLocation/CoreLocation.h>

You should also define a CLLocationManager object at this time, and for convenience, you will set the view controller to also be the CLLocationManagerDelegate. This object will act as your “hub” of action for dealing with all location-based services. Your interface file should now look like this:

//  Chapter4SampleProjectViewController.h

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

@interface Chapter4SampleProjectViewController : UIViewController
<CLLocationManagerDelegate> {
    UILabel *labelLocation;
    CLLocationManager *_locationManager;
}

@property (strong, nonatomic) IBOutlet UILabel *labelLocation;

- (IBAction)toggleLocationServices:(id)sender;
@end

Now that the interface has been defined, you can move to the implementation file (.m) and start implementing these methods and objects. The first thing you're going to tackle is the toggleLocationServices:sender: action. When the user touches this control, you'll want to check if location services are available to you. If they are not available, you will present an alert view stating that location services must be enabled to continue.

if(![CLLocationManager locationServicesEnabled]){
        UIAlertView *alertLocation = [[UIAlertView alloc] initWithTitle:@"Location
Error" message:@"Location services must be enabled for this feature to work"
delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alertLocation show];
        return;
    }

If the location service is available, you will need to check the status of the control. If it has been set to “on”, you'll want to check if your CLLocationManager object has been instantiated. If it hasn't, you will want to instantiate it and set its properties and delegate. For the standard location service, you should always set the desiredAccuracy and distanceFilter properties of the CLLocationManager object.

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 being as conservative as possible with this setting. If you don't need to know the street address of the current device, use a higher accuracy setting. There are a number of constants available to use for your convenience:

kCLLocationAccuracyBestForNavigation
kCLLocationAccuracyBest
kCLLocationAccuracyNearestTenMeters
kCLLocationAccuracyHundredMeters
kCLLocationAccuracyKilometer
kCLLocationAccuracyThreeKilometers

As you can see, those constants that specify distances are restricted to the metric system. If you are not quite familiar with these distance units, a meter is slightly longer than a yard, and a kilometer is just over half (6/10) of a mile.

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 will report all changes in location to your delegate.

One other property that you should always set when using location services is the purpose property. When the user is prompted to allow your application access to his or her location, the string in the purpose property is displayed, telling the user what you plan to do with his or her device's location information.

Once you've set the properties and the delegate, you can start the location services by calling the startUpdatingLocation method on your CLLocationManager.

Stopping the location services is a matter of calling stopUpdatingLocation on the CLLocationManager object. You will want to do this if they flip the UISwitch to off. After adding code to start and stop your CLLocationManager, your action method should look like this:

- (IBAction)toggleLocationServices:(id)sender {
    //Display an UIAlertView if locationServices are not enabled and return
    if(![CLLocationManager locationServicesEnabled]){
        UIAlertView *alertLocation = [[UIAlertView alloc] initWithTitle:@"Location
Error" message:@"Location services must be enabled for this feature to work"
delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alertLocation show];
        return;
    }
    
    //Future Proof: Make sure it's a UISwitch calling this action
    if([sender isKindOfClass:[UISwitch class]]){
        UISwitch *locationSwitch=(UISwitch *)sender;
        //Check if switch is "on"
        if(locationSwitch.on){
            //Check if _locationManager has been instantiated yet
            if(_locationManager==nil){
                //Instantiate _locationManager
                _locationManager = [[CLLocationManager alloc] init];
                _locationManager.desiredAccuracy=kCLLocationAccuracyBest;
                _locationManager.distanceFilter=1;
_locationManager.purpose=@"We will only use your location information to display your
present location.  We will not send it or record it.";
_locationManager.delegate=self;
            }
            //Start updating location
            [_locationManager startUpdatingLocation];
        }else{
            //Check if _locationManager has been instantiated yet
            if(_locationManager!=nil){
                //Stop updating location
                [_locationManager stopUpdatingLocation];
            }
        }
    }
}

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. You will work with the error delegate method first. The most common source of an error is when the user is prompted to allow location services for your app and the user declines to allow your app access to his or her location. If this happens, you will stop requesting the location updates by calling the stopUpdatingLocation method:

-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
    if(error.code == kCLErrorDenied){
[manager stopUpdatingLocation];
    }
}

The delegate method that handles location updates is a little more involved. The method, - locationManager:didUpdateToLocation:fromLocation:, delivers three objects: the CLLocationManager that made the location update request, the newLocation, and the oldLocation. The two location objects are of class CLLocation. This object contains a lot of valuable information, including the location coordinate, accuracy information, and the timestamp of the location update. You will implement this method like so:

-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation
*)newLocation fromLocation:(CLLocation *)oldLocation{
    //Check to make sure this is a recent location event
    NSDate *eventDate=newLocation.timestamp;
    NSTimeInterval eventInterval=[eventDate timeIntervalSinceNow];
    if(abs(eventInterval)<30.0){
        //Check to make sure the event is accurate
        if(newLocation.horizontalAccuracy>=0 && newLocation.horizontalAccuracy<20){
            self.labelLocation.text=newLocation.description;
        }
    }
}

Before your app processes a CLLocation object, you want to check that 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 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. To do this, you can use code similar to the following to process only location events that have occurred within 20 seconds of the current time:

    //Check to make sure this is a recent location event
    NSDate *eventDate=newLocation.timestamp;
    NSTimeInterval eventInterval=[eventDate timeIntervalSinceNow];
    if(abs(eventInterval)<30.0){
        //…process 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 CLLocation object contains two accuracy properties: horizontalAccuracy and verticalAccuracy.

The horizontalAccuracy property represents the radius of the circle, in meters, that the location could be located within. 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.

Here is some sample code to handle horizontalAccuracy:

        if(newLocation.horizontalAccuracy>=0 && newLocation.horizontalAccuracy<20){
            //…process event
        }

One more property that I want to discuss is the description property. The description property returns the location information of a CLLocation object in an NSString format. It is a very easy method for seeing what location information is being returned by the device. I don't recommend showing this string to the end user directly, as it contains a great deal of information, and thus is not well formatted for display to the user, but it could be useful for debugging and verifying that location information is being updated and is correct/accurate. For this project, you have set your labelLocation text to the newLocation.description value, resulting in the previous completed delegate method.

With Xcode 4.2, you can now simulate location information in the iOS simulator. Prior to this fantastic feature, you would load up your application onto your test device and then go running outside to test out the location features. Most of the time you would misssomething and have to do this over and over again. However, it certainly was a great way to get developers out of their chairs and into the sunlight.

Launch the Chapter4SampleProject on the iOS simulator. When you touch the UISwitch to “On”, you will be prompted to allow this application access to your device's location. You will notice in Figure 4–7 that the string you set in the purpose property of your CLLocationManager is displayed. Click OK to continue.

Image

Figure 4–7. Your application requesting location permissions

After clicking OK, you will notice that your labelLocation is not updating even though the UISwitch is on, as in Figure 4–8.

Image

Figure 4–8. Simulated application without any location data

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 labelLocation should start to update with information about the pre-recorded drive that Apple has provided. Figure 4–9 shows a sample of information delivered by the simulated drive.

Image

Figure 4–9. Displaying simulated location information

Recipe 4–2: Significant Location Changes

The significant location change service provides two significant benefits: it is fast and it can run in the background. A lot of the code used between the two location services is the same, and the setup is virtually identical, but I will point out the differences. Let's start a new single view project named Chapter4SignificantLocationTracker, with an identically named class prefix if your version of Xcode allows.

Start by adding the Core Location framework to the project. If you need a reminder of how to add this framework, go back to the previous section to see the steps.

To enable background location services, you need to add a key to the Info.plist. Select the project in the navigator pane, and select your project's target. Switch over to the Info tab, and add a row to the Custom iOS Target Properties listing. The key you want to add is “Required background modes” or UIBackgroundModes. Now add the “App registers for location updates” to Item 0. Your result should resemble Figure 4–10.

Image

Figure 4–10. Specifying location changes as a required background mode

You are going to set up the .xib file exactly as you did in the previous section, as shown by Figure 4–11.

Image

Figure 4–11. Your familiar user interface

Connect the location label to an outlet named labelLocation, and connect the UISwitch to an action named toggleLocationServices, as shown in Figures 4–12 and 4–13.

Image

Figure 4–12. Connecting your newest UILabel to an outlet

Image

Figure 4–13. Creating your toggling action

Your interface file (.h) is going to be set up exactly the same:

//  Chapter4SignificantLocationTrackerViewController.h

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

@interface Chapter4SignificantLocationTrackerViewController : UIViewController
<CLLocationManagerDelegate>{
    CLLocationManager *_locationManager;
    UILabel *labelLocation;
}
@property (strong, nonatomic) IBOutlet UILabel *labelLocation;
- (IBAction)toggleLocationServices:(id)sender;

@end

Now switch to your implementation file (.m) and scroll down to the - (IBAction)toggleLocationServices:(id)sender method. You'll start by checking that location services are enabled and presenting a UIAlertView if they are not:

    //Check if location services are enabled
    if(![CLLocationManager locationServicesEnabled]){
        UIAlertView *alertLocation = [[UIAlertView alloc] initWithTitle:@"Location
Services Needed" message:@"Location services are needed to make this app functional"
delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
        [alertLocation show];
        return;
    }

The rest of the code will be very similar to what you have done before, with a few exceptions. The first is that you do not need to specify the desiredAccuracy and distanceFilter properties. You should still specify the purpose property so users will know what you are going to do with their location.

if([sender isKindOfClass:[UISwitch class]]){
        UISwitch *locationSwitch=(UISwitch *)sender;
        if(locationSwitch.on){
            //Check if _locationManager has been instantiated
            if(_locationManager==nil){
                //Instantiate _locationManager
                _locationManager = [[CLLocationManager alloc] init];
                _locationManager.purpose=@"We will only use your location locally.  We
will not record it or send it to anyone";
                _locationManager.delegate=self;
            }
….

The other significant change is that you will call the startMonitoringSignificantLocationChanges method on the location manager when you are ready to start receiving location changes. So now your action looks like this:

- (IBAction)toggleLocationServices:(id)sender {
    //Check if location services are enabled
    if(![CLLocationManager locationServicesEnabled]){
        UIAlertView *alertLocation = [[UIAlertView alloc] initWithTitle:@"Location
Services Needed" message:@"Location services are needed to make this app functional"
delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
        [alertLocation show];
        return;
    }

    //Future proof, make sure sender is UISwitch
    if([sender isKindOfClass:[UISwitch class]]){
        UISwitch *locationSwitch=(UISwitch *)sender;
        if(locationSwitch.on){
            //Check if _locationManager has been instantiated
            if(_locationManager==nil){
                //Instantiate _locationManager
                _locationManager = [[CLLocationManager alloc] init];
                _locationManager.purpose=@"We will only use your location locally.  We
will not record it or send it to anyone";
                _locationManager.delegate=self;
            }
            //Start updating location changes
            [_locationManager startMonitoringSignificantLocationChanges];
        }else{
            if(_locationManager!=nil){
                //Stop monitoring for location changes
                [_locationManager stopMonitoringSignificantLocationChanges];
            }
        }
    }
}

Now you have to set up the delegate methods. The simplest to define is the - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error method:

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
    if(error.code==kCLErrorDenied){
        [manager stopMonitoringSignificantLocationChanges];
    }
}

For the - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation method, you will do something a little different. In addition to updating the location label, you will also generate a local notification so that you can see when a location is updated while your app is not running.

You are going to start by performing checks to make sure the newLocation timestamp is recent and that it is valid (by checking that the horizontalAccuracy is positive):

    NSDate *eventDate=newLocation.timestamp;
    NSTimeInterval eventInterval = [eventDate timeIntervalSinceNow];
    if(abs(eventInterval)<30.0){
        if(newLocation.horizontalAccuracy>=0){
        …

If the app is running, you will not see the UILocalNotification unless you implement the delegate method application:didReceiveLocalNotification: in the app delegate. Instead of doing that now, you are just going to update the location label with the new location description while the app is open with self.labelLocation.text = newLocation.description;.

To create the UILocalNotification, you'll use the following recipe:

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

The complete code of your delegate method is as follows:

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation
*)newLocation fromLocation:(CLLocation *)oldLocation{
    NSDate *eventDate=newLocation.timestamp;
    NSTimeInterval eventInterval = [eventDate timeIntervalSinceNow];
    NSLog(@"Event Interval: %f", eventInterval);
    NSLog(@"Accuracy: %f", newLocation.horizontalAccuracy);
    if(abs(eventInterval)<30.0){
        if(newLocation.horizontalAccuracy>=0){
            self.labelLocation.text = newLocation.description;
            UILocalNotification *locationNotification = [[UILocalNotification alloc]
init];
            locationNotification.alertBody=[NSString stringWithFormat:@"New Location:
%.3f, %.3f", newLocation.coordinate.latitude, newLocation.coordinate.longitude];
            locationNotification.alertAction=@"Ok";
            locationNotification.soundName = UILocalNotificationDefaultSoundName;
            //Increment the applicationIconBadgeNumber
            locationNotification.applicationIconBadgeNumber=[[UIApplication
sharedApplication] applicationIconBadgeNumber]+1;
            [[UIApplication sharedApplication]
presentLocalNotificationNow:locationNotification];
        }
    }
}

Upon this new app, you will be able to receive local notifications for each significant location change, even while the application is not in the foreground. These changes will then be reflected in a notification badge on the app's icon, as well as a normal device notification.

Recipe 4–3: Determining Magnetic Bearing

Modern iPhones and iPad 2s now contain hardware, the magnetometer, which can be used to determine which direction 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–60km per year toward the west as the earth's core changes.

Implementing heading tracking is very similar to implementing any of the location tracking services we have discussed so far. You will include the Core Location framework in your project, create a CLLocationManager object, and define its delegate and delegate methods.

By default, it is assumed that the device heading is measured with the device held in portrait mode with the top of the device away from the user. You can change this setting by setting the headingOrientation property on the CLLocationManager object. The options for this property are as follows:

CLDeviceOrientationPortrait (default)
CLDeviceOrientationPortraitUpsideDown
CLDeviceOrientationLandscapeLeft
CLDeviceOrientationLandscapeRight

Let us start by creating a new single view application project named Chapter4HeadingTracking. If your version of Xcode allows for specifying a class prefix, supply “Chapter4HeadingTracking” again. You start by including the Core Location framework in your project by selecting the project in the navigator pane and selecting the project's target. Add the Core Location framework into the project just as you did at the beginning of the chapter.

Next you can set up the XIB to display your heading. Click the Chapter4HeadingTrackingViewController.xib file in the navigator pane, and Interface Builder will be loaded. Like with the previous recipes, you are going to drag a UILabel and UISwitch object onto the XIB. The UILabel will be used to display the heading information, and the UISwitch will be used to turn heading tracking services on and off. You're going to set the initial state of the UISwitch to Off in the Attributes inspector pane. Your XIB now looks like Figure 4–14.

Image

Figure 4–14. Your still–familiar user interface setup

With the .xib file laid out, let's create some outlet properties and actions. Turn on the Assistant Editor and select the UIView in the XIB. ^-click-drag from the UILabel to the interface file (.h) to create an outlet. In the pop-up that is displayed, name the UILabel “labelHeading”. These steps should resemble Figures 4–15 and 4–16.

Image

Figure 4–15. Connecting a UILabel outlet

Image

Figure 4–16. Configuring the label's outlet

Repeat the same process with the UISwitch, but this time change the Connection type to Action and name the action “switchHeadingServices”, as shown in Figure 4–17.

Image

Figure 4–17. Creating your switch's action

In your interface file (.h) for Chapter4HeadingTrackingViewController, import the Core Location framework and set the view controller to comply with the CLLocationManagerDelegate protocol. Then define a CLLocationManager instance variable. The interface file should now resemble the following:

//  Chapter4HeadingTrackingViewController.h

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

@interface Chapter4HeadingTrackingViewController : UIViewController <CLLocationManagerDelegate>{
    CLLocationManager *_locationManager;
    UILabel *labelHeading;
}

@property (strong, nonatomic) IBOutlet UILabel *labelHeading;
– (IBAction)switchHeadingServices:(id)sender;
@end

Switch to the implementation file (.m), and scroll to the bottom to start defining the switchHeadingService method. As with location services, you will start by checking that heading services are available by checking the return of [CLLocationManager headingAvailable]. Then you will verify that the sender is UISwitch:

if([CLLocationManager headingAvailable]){
        if([sender isKindOfClass:[UISwitch class]]){
            UISwitch *headingSwitch=(UISwitch *)sender;
        …

If the headingSwitch is “on”, you will check to make sure the instance variable _locationManager is instantiated and instantiate it if it has not been. 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) that your heading has to change before your delegate method is called. Then, as with the other location tracking services, you will specify the purpose property to tell the user what you intend to use the location information for and finally the delegate of the CLLocationManager. Once the instance variable has been instantiated, you can call startUpdatingHeading to start the heading tracking services, and you will also update your label so you can see the progress you are making:

if(headingSwitch.on){
                if(_locationManager==nil){
                    _locationManager=[[CLLocationManager alloc] init];
                    _locationManager.headingFilter=5;
                    _locationManager.purpose=@"We will use your location to tell you
where you are headed";
                    _locationManager.delegate=self;
                }
                [_locationManager startUpdatingHeading];
                self.labelHeading.text=@"Starting heading tracking…";
}else{
       ….

Next you will turn off heading tracking services if the switch has been moved to the off position, so the completed method looks like this:

- (IBAction)switchHeadingServices:(id)sender {
    if([CLLocationManager headingAvailable]){
        if([sender isKindOfClass:[UISwitch class]]){
            UISwitch *headingSwitch=(UISwitch *)sender;
            if(headingSwitch.on){
                if(_locationManager==nil){
                    _locationManager=[[CLLocationManager alloc] init];
                    _locationManager.headingFilter=5;
                    _locationManager.purpose=@"We will use your location to tell you
where you are headed";
                    _locationManager.delegate=self;
                }
                [_locationManager startUpdatingHeading];
                self.labelHeading.text=@"Starting heading tracking…";
            }else{
                self.labelHeading.text=@"Turned heading tracking off";
                if(_locationManager!=nil){
                    [_locationManager stopUpdatingHeading];
                }
            }
        }
    }else{
        self.labelHeading.text=@"Heading services unavailable";
    }
}

The delegate methods need to be defined next. With heading tracking services, there are three delegate methods that 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. You want to turn off heading tracking services if there is an error (most likely caused because the user is not granting you access to his or her location services):

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
    if(error.code==kCLErrorDenied){
        [manager stopUpdatingHeading];
        self.labelHeading.text=@"ERROR: Heading tracking is denied";
    }
}

The next method, didUpdateHeading, handles when the change in heading of the device has exceeded your headingFilter property. In your instance, you are going to check the timestamp to make sure it is a recent reading, and then you are going to make sure the headingAccuracy property is positive. The headingAccuracy property will be negative if the heading is invalid. Finally, you will update the labelHeading with the magneticHeading reading.

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

        self.labelHeading.text=[NSString stringWithFormat:@"Your new heading is: %.1f°",
newHeading.magneticHeading];
    }
}

NOTE: Use Image+Image+8 (Option+Shift+8) to insert the degree (°) symbol.

The last 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 so that it can calibrate the magnetometer. I haven't run into an instance where I would not want to display this scene yet, so I always return YES:

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

Figure 4–18 shows what the calibration screen looks like if the device is unable to easily calibrate. When testing this application, you may not actually see this screen, but it is important to have it enabled as a precaution.

Image

Figure 4–18. Heading calibration screen

This is an application that will not work in the simulator. You will have to load it onto an actual device to test it.

Recipe 4–4: Specifying 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 declination. And with an iPhone or iPad 2, if the device knows where it is located, the device will do the calculation for you and provide it in the trueHeading property of a CLHeading object.

To clarify, if you combine location-tracking services with heading tracking services, you can find out a device's orientation in reference to true north. All you need to do is also call the startUpdatingLocation method on your CLLocationManager to get the true north heading.

You can start a new project or expand on the project from the previous recipe. Select the Chapter4HeadingTrackingViewController.xib file, and add a second label for the true heading, as in Figure 4–19.

Image

Figure 4–19. New user interface with an added label

Connect this new label to an outlet named “labelTrueHeading” by showing the Assistant Editor pane and doing a ^-click-drag from the label to the interface file (.h), as in Figure 4–20.

Image

Figure 4–20. Configuring your additional label's outlet

Your interface file (.h) should now look like the following block. If your version of Xcode does not create the UILabel instance variables in addition to the properties, do not worry, as these are optional.

//  Chapter4HeadingTrackingViewController.h
//  Chapter4HeadingTracking

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

@interface Chapter4HeadingTrackingViewController : UIViewController
<CLLocationManagerDelegate>{
    CLLocationManager *_locationManager;
    UILabel *labelHeading;
    UILabel *labelTrueHeading;
}


@property (strong, nonatomic) IBOutlet UILabel *labelHeading;
@property (strong, nonatomic) IBOutlet UILabel *labelTrueHeading;
- (IBAction)switchHeadingServices:(id)sender;
@end

There are only some minor changes that need to be made to your code. First, you need to call startUpdatingLocation and stopUpdatingLocation in your switchHeadingServices method:

if(headingSwitch.on){
        if(_locationManager==nil){
                _locationManager=[[CLLocationManager alloc] init];
                _locationManager.headingFilter=5;
                _locationManager.purpose=@"We will use your location to tell you where
you are headed";
                _locationManager.delegate=self;
        }
        [_locationManager startUpdatingHeading];
        [_locationManager startUpdatingLocation];
        self.labelHeading.text=@"Starting heading tracking…";
}else{
        self.labelHeading.text=@"Turned heading tracking off";
        if(_locationManager!=nil){
                [_locationManager stopUpdatingHeading];
                [_locationManager stopUpdatingLocation];
        }
}

Now, in your didUpdateHeading method, you will add a new statement to check the trueHeading value. If the trueHeading is negative, it is invalid, so you want to use the trueHeading property only if it is greater than or equal to 0:

if(newHeading.headingAccuracy<0)
            return;
        
        if(newHeading.trueHeading>=0){
            self.labelTrueHeading.text=[NSString stringWithFormat:@"Your true heading
is: %.1f°", newHeading.trueHeading];
        }
        
        self.labelHeading.text=[NSString stringWithFormat:@"Your magnetic heading is:
%.1f°", newHeading.magneticHeading];

Your custom methods in your implementation file (.m) look like the following:

//
//  Chapter4HeadingTrackingViewController.m

#import "Chapter4HeadingTrackingViewController.h"

@implementation Chapter4HeadingTrackingViewController
@synthesize labelHeading;
@synthesize labelTrueHeading;
- (IBAction)switchHeadingServices:(id)sender {
    if([CLLocationManager headingAvailable]){
        if([sender isKindOfClass:[UISwitch class]]){
            UISwitch *headingSwitch=(UISwitch *)sender;
            if(headingSwitch.on){
                if(_locationManager==nil){
                    _locationManager=[[CLLocationManager alloc] init];
                    _locationManager.headingFilter=5;
                    _locationManager.purpose=@"We will use your location to tell you
where you are headed";
                    _locationManager.delegate=self;
                }
                [_locationManager startUpdatingHeading];
                [_locationManager startUpdatingLocation];
                self.labelHeading.text=@"Starting heading tracking…";
            }else{
                self.labelHeading.text=@"Turned heading tracking off";
                if(_locationManager!=nil){
                    [_locationManager stopUpdatingHeading];
                    [_locationManager stopUpdatingLocation];
                }
            }
        }
    }else{
        self.labelHeading.text=@"Heading services unavailable";
    }
}

-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
    if(error.code==kCLErrorDenied){
        [manager stopUpdatingHeading];
        self.labelHeading.text=@"ERROR: Heading tracking is denied";
    }
}

-(void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading
*)newHeading{
    NSDate *headingDate=newHeading.timestamp;
    NSTimeInterval headingInterval=[headingDate timeIntervalSinceNow];
    if(abs(headingInterval)<30){
        if(newHeading.headingAccuracy<0)
            return;
        
        if(newHeading.trueHeading>=0){
            self.labelTrueHeading.text=[NSString stringWithFormat:@"Your true heading
is: %.1f°", newHeading.trueHeading];
        }
        
        self.labelHeading.text=[NSString stringWithFormat:@"Your magnetic heading is:
%.1f°", newHeading.magneticHeading];
    }
}

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

Upon testing this application, you will be able to get a simple readout of your device's headings, both based on magnetic heading and true heading. Like the other recipes in this chapter that make use of the magnetometer, this functionality will work only on a physical device, and not the simulator.

Recipe 4–5: Region Monitoring

Core Location provides a method for monitoring when a device enters or exits a circular region. This could be used by an application to 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. There are many possibilities 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 (again, a meter is just over three feet or a yard). 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:desiredAccuracy: 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 of the regions you set up previously will be available in the monitoredRegions property of the CLLocationManager object.

Regions are shared system-wide, and there is a limited number of regions that 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 kCLErrorRegionMonitoringFailure will 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, MD and welcome visitors to the city when they enter it. You will start by creating a new single view application named “Chapter4RegionMonitoring”. If your version of Xcode allows for the setting of a class prefix, use the same name of “Chapter4RegionMonitoring”. The first thing to do is include the Core Location framework in your project by selecting the project in the navigator pane and then selecting the project's target. Go to the Build Phases tab, and expand the Link Binary With Libraries area to see the included frameworks, and add the framework as you have done in the previous sections.

With the library added, you can create the .xib file. Select the Chapter4RegionMonitoringViewController.xib in the navigator pane to open up Interface Builder. Add a UILabel and a UISwitch. Set the UISwitch initial state to “Off”. It should resemble Figure 4–21.

Image

Figure 4–21. Your quite familiar user interface

Using the Assistant Editor, connect the outlets and actions by doing a ^-click-drag from the .xib file to the interface file (.h). Create a UILabel outlet named “labelRegionInfo” and an IBAction for the UISwitch named “regionMonitoringToggle”, following Figures 4–22, 4–23, 4–24, and 4–25.

Image

Figure 4–22. Connecting a UILabel to an outlet

Image

Figure 4–23. Configuring UILabel outlet

Image

Figure 4–24. Connecting a UISwitch's action

Image

Figure 4–25. Configuring the toggling action

You need to import the Core Location framework into your interface file (.h) and create a CLLocationManager instance variable. Then declare your view controller as complying with the CLLocationManagerDelegate protocol. Your interface file (.h) for the view controller now looks like this:

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

@interface Chapter4RegionMonitoringViewController : UIViewController
<CLLocationManagerDelegate>{
    CLLocationManager *_locationManager;
    UILabel *labelRegionInfo;
}

@property (strong, nonatomic) IBOutlet UILabel *labelRegionInfo;
- (IBAction)regionMonitoringToggle:(id)sender;

@end

Switching to the implementation file (.m), you can implement your region tracking methods. The first thing you will make sure of is that region monitoring is available and enabled. Scroll to the bottom of Chapter4RegionMonitoringViewController.m and add the following to the - (IBAction)regionMonitoringToggle:(id)sender method:

//Check if region monitoring is available
if([CLLocationManager regionMonitoringAvailable] && [CLLocationManager
regionMonitoringEnabled]){

        ….
}else{
        self.labelRegionInfo.text=@"Region monitoring is not available on this device";
}

In the same method, you will want to instantiate your CLLocationManager instance variable if it is not already created and make sure that it is your UISwitch calling the method:

    //Check if region monitoring is available
    if([CLLocationManager regionMonitoringAvailable] && [CLLocationManager
regionMonitoringEnabled]){
        //Make sure sender is UISwitch
        if([sender isKindOfClass:[UISwitch class]]){
            UISwitch *regionSwitch=(UISwitch *) sender;
            //If UISwitch is turned On
            if(regionSwitch.on){
                if(_locationManager==nil){
                    _locationManager=[[CLLocationManager alloc] init];
                    _locationManager.purpose=@"To welcome you to Baltimore";
                    _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;
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 telling your CLLocationManager instance object about the region and setting the accuracy at which you want to monitor for the event:

[_locationManager startMonitoringForRegion:baltimoreRegion
        desiredAccuracy:kCLLocationAccuracyHundredMeters];

One last thing you want to do is turn off region monitoring if the user slides the UISwitch to the “Off” position. To do this, you will access the monitoredRegions property of your CLLocationManager instance variable and turn off region monitoring for all of the currently monitored regions. You could also choose to selectively turn off specific regions by utilizing the identifier property of the CLRegion.

}else{
                //If UISwitch is turned Off
                if(_locationManager!=nil){
                    for (CLRegion *monitoredRegion in [_locationManager
monitoredRegions]) {
                        [_locationManager stopMonitoringForRegion:monitoredRegion];
                        self.labelRegionInfo.text=[NSString stringWithFormat:@"Turned
off region monitoring fore : %@", 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.labelRegionInfo.text=@"Region monitoring is denied on this device";
            break;
        }   
        case kCLErrorRegionMonitoringFailure:
        {
            self.labelRegionInfo.text=[NSString stringWithFormat:@"Region monitoring
failed for region: %@", region.identifier];
            break;
        }
        default:
        {
            self.labelRegionInfo.text=[NSString stringWithFormat:@"An unhandled error
occured: %@", error.description];
            break;
        }
    }
}

Did enter and did exit can perform any function that you want, and since 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.labelRegionInfo.text = @"Welcome to Baltimore!";
    UILocalNotification *locationNotification = [[UILocalNotification alloc] init];
    locationNotification.alertBody=@"Welcome to Baltimore!";
    locationNotification.alertAction=@"Ok";
    locationNotification.soundName = UILocalNotificationDefaultSoundName;
    [[UIApplication sharedApplication]
presentLocalNotificationNow:locationNotification];
}
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region{
    self.labelRegionInfo.text = @"Thanks for visiting Baltimore! Come back soon!";
    UILocalNotification *locationNotification = [[UILocalNotification alloc] init];
    locationNotification.alertBody=@"Thanks for visiting Baltimore! Come back soon!";
    locationNotification.alertAction=@"Ok";
    locationNotification.soundName = UILocalNotificationDefaultSoundName;
    [[UIApplication sharedApplication]
presentLocalNotificationNow:locationNotification];
    
}

In order to test this functionality using the iOS simulator, you must be able to feed custom coordinates in 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.

Recipe 4–6: Reverse and Forward 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, etc. 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 for converting location coordinates into a human-readable format. The method is called reverse geocoding, and the Core Location framework in iOS 5 provides it. In versions of iOS prior to 5, the Map Kit framework provided this feature.

Here are some best practices to be aware of when using reverse 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 if the user has moved a significant distance before calling another geocoding request.
  • Do not perform a geocoding request if the user will not see the results (such as if your application is running in the background).
  • A device must have network access to perform a geocoding request.

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 that will tell us where we are called “Chapter4Geocoder”. If you can specify a class prefix, use the same name of “Chapter4Geocoder”. Add the Core Location framework to the application by selecting the project in the navigator pane and selecting the target. Now click the Build Settings tab, expand the area labeled Link Binary With Libraries, and add the Core Location framework as you did in previous recipes.

Select the Chapter4GeocoderViewController.xib file to load Interface Builder, and add a UILabel and rounded UIButton to the XIB, resembling Figure 4–26.

Image

Figure 4–26. Geocoding user interface

Using the Assistant Editor, ^-click-drag the UILabel to the interface file (.h) to create an outlet named “labelGeocodeInfo”. Repeat the process with the UIButton to an action named “actionWhereAmI”. These steps are demonstrated by Figures 4–27, 4–28, and 4–29.

Image

Figure 4–27. Connecting your UILabel's outlet

Image

Figure 4–28. Connecting a UISwitch's action

Image

Figure 4–29. Configuring an action's creation

Import the Core Location library into the Chapter4GeocoderViewController interface file (.h), and define the view controller as complying with the CLLocationManagerDelegate protocol. This is used only to get the device's current location; it is not necessary for geocoding.

You will also create an instance variable of class CLLocationManager to get the current location of the device and an instance of CLGeocoder to perform the geocoding. When completed, the interface file should resemble this:

//  Chapter4GeocoderViewController.h

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

@interface Chapter4GeocoderViewController : UIViewController
<CLLocationManagerDelegate>{
    CLLocationManager *_locationManager;
    CLGeocoder *_geoCoder;
    UILabel *labelGeocodeInfo;
}

@property (strong, nonatomic) IBOutlet UILabel *labelGeocodeInfo;
- (IBAction)actionWhereAmI:(id)sender;
@end

Switch to the Chapter4GeocoderViewController implementation file (.m), and scroll to the bottom to implement the method actionWhereAmI. Since this has been covered in previous recipes in this chapter, I'm not going to go into detail about this, but I 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)actionWhereAmI:(id)sender {
    if([CLLocationManager locationServicesEnabled]){
        if([sender isKindOfClass:[UIButton class]]){
            if(_locationManager==nil){
                _locationManager=[[CLLocationManager alloc] init];
                _locationManager.purpose=@"To tell you where you are";
                _locationManager.delegate=self;
                _locationManager.distanceFilter=500;
                _locationManager.desiredAccuracy=kCLLocationAccuracyHundredMeters;
            }
            [_locationManager startUpdatingLocation];
            self.labelGeocodeInfo.text=@"Getting location…";
        }
    }else{
        self.labelGeocodeInfo.text=@"Location services are unavailable";
    }
}

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

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

Next is the didUpdateToLocation delegate method to be defined. You will start with the standard checks to make sure the newLocation timestamp property is recent and that it is accurate:

-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation
*)newLocation fromLocation:(CLLocation *)oldLocation{
    NSDate *locationDate=newLocation.timestamp;
    NSTimeInterval locationInterval=[locationDate timeIntervalSinceNow];
    if(abs(locationInterval)<30){
        if(newLocation.horizontalAccuracy<0)
            return;

You will check if the _geoCoder instance variable has been instantiated, and if not, you will create it. Then you will also make sure that you stop any existing geocoding services before performing a new one:

//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, you will start your reverse geocoding process and define the completion handler. The completion handler receives two objects, an NSArray of CLPlacemarks named placemarks and an NSError. If the array contains one or more objects, then the reverse geocode was successful. If not, then you can check the error code for details. The resulting didUpdateToLocation method is as follows:

-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation
*)newLocation fromLocation:(CLLocation *)oldLocation{
    NSDate *locationDate=newLocation.timestamp;
    NSTimeInterval locationInterval=[locationDate timeIntervalSinceNow];
    if(abs(locationInterval)<30){
        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.labelGeocodeInfo.text=[NSString stringWithFormat:@"You
are in: %@", foundPlacemark.description];
                        }else if(error.code==kCLErrorGeocodeCanceled){
                            NSLog(@"Geocoding cancelled");
                        }else if(error.code==kCLErrorGeocodeFoundNoResult){
                            self.labelGeocodeInfo.text=@"No geocode result found";
                        }else if(error.code==kCLErrorGeocodeFoundPartialResult){
                            self.labelGeocodeInfo.text=@"Partial geocode result";
                        }else{
                            self.labelGeocodeInfo.text=[NSString
stringWithFormat:@"Unknown error: %@", error.description];
                        }
                    }];

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

Upon testing this application, you should be able to receive the location, including street name, city, country, and other valuable information of the given device. This can of course be tested on a physical device, or in the simulator using the same location-simulating functions you have used previously.

Getting Coordinates from Place Names

iOS 5 has introduced forward geocoding as well. This means that you can pass a CLGeocoder object an address and receive the coordinates for that address as a result. The CLGeocoder processes address strings as a parameter, and the more information you can provide about an address, the more accurate the resulting forward geocode will be. If the geocode process results in multiple coordinates being identified as possible matches, these coordinates will be returned in the NSArray placemarks of the completionHandler. A sample implementation of this could be as follows:

CLGeocoder *_geoCoder=[[CLGeocoder alloc] init];
[_geoCoder geocodeAddressString:@”2400 Boston Street, Baltimore, MD, USA"
        completionHandler:^(NSArray* placemarks, NSError* error){
                        for (CLPlacemark* aPlacemark in placemarks)
                        {
                                // Process the placemark.
                        }
}];

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. The use of the purpose property on a CLLocationManager object and judicious use of location services are steps in the right direction.

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

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