Wednesday, January 16, 2013

iOS Development From The Frontlines Core Location in iOS: A Tutorial

Core Location in iOS: A Tutorial



Developing iOS apps that can take advantage of the built-in GPS unit is a great way to add a new dimension to the user experience. You can quickly make your apps location-aware (and able to find the current latitude/longitude coordinates) with minimal effort. This tutorial will walk through the steps involved in finding a device’s current position.
In iOS, you can find the current lat/lng by making a request to the Core Location component to start looking up location information. This works via a series of delegate messages passed to our application when Core Location starts updating. By default, the location hardware in our device is usually turned off (or otherwise in an idle state) so as not to needlessly drain power. In a nutshell we need to:
  1. #import the correct Core Location header file
  2. Link against the Core Location framework
  3. Implement two delegate methods to receive information from Core Location
  4. Tell Core Location to start sending us updates
  5. Properly shut down Core Location when we don’t need it anymore.

Getting Started

Since we’re going to be using Core Location with our iOS app, we need to add the header file (CoreLocation/CoreLocation.h) to our prefix header (usually with the extension .pch). In this prefix header, locate the #ifdef __OBJC__ line:
#ifdef __OBJC__
    #import 
    #import 
    #import  // added for Core Location
#endif
If you have a larger project and are only using Core Location in a few places, you could alternatively just add the #import line to the files where you actually need it. In our simple example, we’ll stick with the prefix file.
Next, we need to add the Core Location framework to our project’s link phase. In Xcode, click on the project’s name at the top of the project navigator view. In the Linked Frameworks and Libraries section, click the “+” icon to add a new framework. Find the Core Location.framework entry, and add it. When you next link your project, the framework will be automatically included.


Setting up the Delegate

The begin setting up our class as a delegate, the first step is to locate the class where we want to receive Core Location messages. Then, we need to set that class up as a CLLocationManagerDelegate. You can do this by adding to the @interface definition line.
Next, in the implementation file, we need to add two delegate methods:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
This method will be called when Core Location has new location data for us to process. We receive these messages both when we have better accuracy for a current location, or the device has moved. These messages can also come in quickly one after the other, or as a trickle.
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
If Core Location needs to report a failure of some kind, this method will be called. We need to properly handle these. Note that this method is technically optional, but it’s best to get into the habit of handling these messages.
Next, we also need a way to store where we currently are located. Since there’s no guarantee of when we’ll be told of our location, we need to store this information for the times when we need to reference it. We also need to keep track of our Core Location manager instance. So, be sure to add the following to your class’ @interface definition:
@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong, nonatomic) CLLocation *currentLocation;
And, in the implementation file, the corresponding synthesize methods:
@synthesize locationManager, currentLocation;

Receiving Messages

Now that the setup is out of the way, it’s on to the fun stuff. First, let’s implement the didUpdateToLocation method. I’ll paste the entirety here and then we’ll step through it:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
    self.currentLocation = newLocation;
 
    if(newLocation.horizontalAccuracy <= 100.0f) { [locationManager stopUpdatingLocation]; }
}
First, note that we’re saving the currentLocation. Again, since we don’t know when we’re going to be given an update to our location, we need to keep this around for whenever we might need it in the future.
Next, we check the included accuracy against a pre-set expectation. Each time we’re given a newLocation, we’re given an estimation on how accurate it is. This float is in meters, and usually starts out as pretty inaccurate (remember that when we first start requesting location updates, Core Location needs to determine the device’s location, which can take time).
You’ll notice in the Maps app on your iPhone that a blue circle is initially drawn around the point marking your location. That blue circle’s radius is determined by this horizontalAccuracy number. If you’re going to be showing a point on a map for the user’s current location, you may want to consider adding a similar “accuracy circle”.
The reason why we’re checking the provided accuracy against a pre-set expectation is that we don’t want to leave Core Location running for any longer than we absolutely need it to. It drains the battery life of the device when it has to communicate with GPS satellites or power up the radio for location triangulation, so we want to turn it back off again as soon as we have a good-enough position. In the example above, a position +/- 100m is plenty accurate for us. To stop Core Location from sending us updates (and thus allowing it to go back into an idle state), we send our locationManager the stopUpdatingLocation message.
Lastly, we need to implement the didFailWithError method. Here it is:
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
    if(error.code == kCLErrorDenied) {
        [locationManager stopUpdatingLocation];
    } else if(error.code == kCLErrorLocationUnknown) {
        // retry
    } else {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error retrieving location"
                              message:[error description]
                              delegate:nil
                              cancelButtonTitle:@"OK"
                              otherButtonTitles:nil];
        [alert show];
    }
}
When handling errors from Core Location, we need to worry about two special cases. The first is when the user specifically denies our app access to finding its position. If that occurs, we want to immediately stop trying to update by sending the stopUpdatingLocation message.
The second special case is when we’re given a kCLErrorLocationUnknown error. This can occur when Core Location is first starting up and isn’t able to immediately determine its position, or even make a guess as to where it is. If we get this message, its best to ignore it and to keep trying to update our location.
In other cases, we may want to alert the user to a problem with getting a location. According to the Core Location docs, this might occur when the device is near a particularly strong magnetic field. In this example, we’re showing the user a problem but still retrying. You will need to determine for your own app whether you want to stop trying to get location updates at this point or to keep retrying.

Requesting Updates

After the delegate has been implemented, it’s time to actually setup Core Location and request location updates. To setup Core Location, you’ll want to do the following:
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
[locationManager startUpdatingLocation];
This sets up a new CLLocationManager instance and saves a pointer to it in a class variable. We next set our current class up as the delegate to receive CLLocationManagerDelegate messages. And finally, we tell Core Location to start figuring out our position.
In my example app, I’ve placed all of my Core Location-related code in a single view controller’s class as I only need this information in one section of my app. Thus, I placed the above three lines in my viewDidLoad method, which is called after the view has been loaded, but before it’s being shown to the user. This could occur right when the app opens up (if loaded from the XIB file), but it could also occur at another point in the app, such as after a low-memory condition and the view had been previously unloaded.
Since I put this setup code in the viewDidLoad method, I also need to remember to tell Core Location to stop updating should my view get unloaded. Thus, I placed the following line in my viewDidUnload method:
[locationManager stopUpdatingLocation];
It’s important to remember to tell Core Location to stop sending messages to the delegate before the delegate could be freed, which could cause Core Location to attempt to send messages to a freed/nil object (which would cause a crash).
And that’s all there is to it! Implementing Core Location is an easy thing to implement and can add an interesting new dimension to your iOS apps. Be sure to post in the comments if you found this interesting or have questions.

No comments:

Post a Comment