Where Am I? Finding Your Way with Core Location and Map Kit - Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (2014)

Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (2014)

Chapter 19. Where Am I? Finding Your Way with Core Location and Map Kit

Every iOS device has the ability to determine where in the world it is using a framework called Core Location. iOS also includes a framework called Map Kit that lets you easily create a live interactive map showing any locations you like, including, of course, the user’s location. In this chapter, we’ll get you started using both of these frameworks.

Core Location can actually leverage three technologies to do this: GPS, cell ID location, and Wi-Fi Positioning Service (WPS). GPS is the most accurate of the three technologies, but it is not available on first-generation iPhones, iPod touches, or Wi-Fi-only iPads. In short, any device with at least a 3G data connection also contains a GPS unit. GPS reads microwave signals from multiple satellites to determine the current location.

Note Technically, Apple uses a version of GPS called Assisted GPS, also known as A-GPS. A-GPS uses network resources to help improve the performance of stand-alone GPS. The basic idea is that the telephony provider deploys services on its network that mobile devices will automatically find and collect some data from. This allows a mobile device to determine its starting location much more quickly than if it were relying on the GPS satellites alone.

Cell ID location lookup gives a rough approximation of the current location based on the physical location of the cellular base station that the device is currently in contact with. Since each base station can cover a fairly large area, there is a fairly large margin of error here. Cell ID location lookup requires a cell radio connection, so it works only on the iPhone (all models, including the very first) and any iPad with a 3G data connection.

The WPS option uses the media access control (MAC) addresses from nearby Wi-Fi access points to make a guess at your location by referencing a large database of known service providers and the areas they service. WPS is imprecise and can be off by many miles.

All three methods put a noticeable drain on the battery, so keep that in mind when using Core Location. Your application shouldn’t poll for location any more often than is absolutely necessary. When using Core Location, you have the option of specifying a desired accuracy. By carefully specifying the absolute minimum accuracy level you need, you can prevent unnecessary battery drain.

The technologies that Core Location depends on are hidden from your application. We don’t tell Core Location whether to use GPS, triangulation, or WPS. We just tell it how accurate we would like it to be, and it will decide from the technologies available to it which is best for fulfilling our request.

The Location Manager

The Core Location API is actually fairly easy to use. The main class we’ll work with is CLLocationManager, usually referred to as the location manager. To interact with Core Location, you need to create an instance of the location manager, like this:

CLLocationManager *locationManager = [[CLLocationManager alloc] init];

This creates an instance of the location manager, but it doesn’t actually start polling for your location. You must create an object that conforms to the CLLocationManagerDelegate protocol and assign it as the location manager’s delegate. The location manager will call delegate methods when location information becomes available or changes. The process of determining location may take some time—even a few seconds.

Setting the Desired Accuracy

After you set the delegate, you also want to set the desired accuracy. As we mentioned, don’t specify a degree of accuracy any greater than you absolutely need. If you’re writing an application that just needs to know which state or country the phone is in, don’t specify a high level of precision. Remember that the more accuracy you demand of Core Location, the more juice you’re likely to use. Also, keep in mind that there is no guarantee that you will get the level of accuracy you have requested.

Here’s an example of setting the delegate and requesting a specific level of accuracy:

locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;

The accuracy is set using a CLLocationAccuracy value, a type that’s defined as a double. The value is in meters, so if you specify a desiredAccuracy of 10, you’re telling Core Location that you want it to try to determine the current location within 10 meters, if possible. Specifying kCLLocationAccuracyBest (as we did previously) or specifying kCLLocationAccuracyBestForNavigation (where it uses other sensor data as well) tells Core Location to use the most accurate method that’s currently available. In addition, you can also usekCLLocationAccuracyNearestTenMeters, kCLLocationAccuracyHundredMeters, kCLLocationAccuracyKilometer, and kCLLocationAccuracyThreeKilometers.

Setting the Distance Filter

By default, the location manager will notify the delegate of any detected change in the device’s location. By specifying a distance filter, you are telling the location manager not to notify you of every change, but instead to notify you only when the location changes by more than a certain amount. Setting up a distance filter can reduce the amount of polling your application does.

Distance filters are also set in meters. Specifying a distance filter of 1000 tells the location manager not to notify its delegate until the iPhone has moved at least 1,000 meters from its previously reported position. Here’s an example:

locationManager.distanceFilter = 1000.;

If you ever want to return the location manager to the default setting, which applies no filter, you can use the constant kCLDistanceFilterNone, like this:

locationManager.distanceFilter = kCLDistanceFilterNone;

Just as when specifying the desired accuracy, you should take care to avoid getting updates any more frequently than you really need them; otherwise, you waste battery power. A speedometer app that’s calculating the user’s velocity based on the user’s location will probably want to have updates as quickly as possible, but an app that’s going to show the nearest fast-food restaurant can get by with a lot fewer updates.

Getting Permission to Use Location Services

Before your application can use location services, you need to get the user’s permission to do so. Core Location offers several different services, some of which can be used even when your application is in the background—in fact, you can even request to have your application launched when certain events happen while it is not running. Depending on what your application does, it may be enough to request permission to access location services only while the user is using your application, or it might need to always be able to use the service. When writing an application, you need to decide which type of permission it requires and you need to make the request before initiating the services that you need. You’ll see how to do this in the course of creating the example application for this chapter.

Starting the Location Manager

When you’re ready to start polling for location, and after you request from the user to access location services, you tell the location manager to start. It will go off and do its thing and then call a delegate method when it has determined the current location. Until you tell it to stop, it will continue to call your delegate method whenever it senses a change that exceeds the current distance filter.

Here’s how you start the location manager:

[locationManager startUpdatingLocation];

Using the Location Manager Wisely

If you need to determine the current location only and you don’t need continuous updates, you should have your location delegate stop the location manager as soon as it gets the information your application requires. If you need to poll, make sure you stop polling as soon as you possibly can. Remember that as long as you are getting updates from the location manager, you are putting a strain on the user’s battery.

To tell the location manager to stop sending updates to its delegate, call stopUpdatingLocation, like this:

[locationManager stopUpdatingLocation];

The Location Manager Delegate

The location manager delegate must conform to the CLLocationManagerDelegate protocol, which defines several methods, all of them optional. One of these methods is called by the location manager when the availability of user authorization to use location services changes, another when it has determined the current location or when it detects a change in location. Yet another method is called when the location manager encounters an error. We’ll implement all of these delegate methods in our app.

Getting Location Updates

When the location manager wants to inform its delegate of the current location, it calls the locationManager:didUpdateLocations: method. This method takes two parameters:

· The first parameter is the location manager that called the method.

· The second parameter is an array of CLLocation objects that describe the current location of the device and perhaps a few previous locations. If several location updates occur in a short period of time, they may be reported all at once with a single call to this method. In any case, the most recent location is always the last item in this array.

Getting Latitude and Longitude Using CLLocation

Location information is passed from the location manager using instances of the CLLocation class. This class has six properties that might be of interest to your application:

· coordinate

· horizontalAccuracy

· altitude

· verticalAccuracy

· floor

· timestamp

The latitude and longitude are stored in a property called coordinate. To get the latitude and longitude in degrees, do this:

CLLocationDegrees latitude = theLocation.coordinate.latitude;
CLLocationDegrees longitude = theLocation.coordinate.longitude;

The CLLocation object can also tell you how confident the location manager is in its latitude and longitude calculations. The horizontalAccuracy property describes the radius of a circle (in meters, like all Core Location measurements) with the coordinate as its center. The larger the value in horizontalAccuracy, the less certain Core Location is of the location. A very small radius indicates a high level of confidence in the determined location.

You can see a graphic representation of horizontalAccuracy in the Maps application (see Figure 19-1). The circle shown in Maps uses horizontalAccuracy for its radius when it detects your location. The location manager thinks you are at the center of that circle. If you’re not, you’re almost certainly somewhere inside the circle. A negative value in horizontalAccuracy is an indication that you cannot rely on the values in coordinate for some reason.

image

Figure 19-1. The Maps application uses Core Location to determine your current location. The outer circle is a visual representation of the horizontal accuracy

The CLLocation object also has a property called altitude that can tell you how many meters above (or below) sea level you are:

CLLocationDistance altitude = theLocation.altitude;

Each CLLocation object maintains a property called verticalAccuracy that is an indication of how confident Core Location is in its determination of altitude. The value in altitude could be off by as many meters as the value in verticalAccuracy. If the verticalAccuracyvalue is negative, Core Location is telling you it could not determine a valid altitude.

The floor property gives the floor within the building in which the user is located. This value is only valid in buildings that are able to provide the information, so you should not rely on its availability.

CLLocation objects also have a timestamp that tells when the location manager made the location determination.

In addition to these properties, CLLocation has a useful instance method that will let you determine the distance between two CLLocation objects. The method is called distanceFromLocation: and it returns a value of type CLLocationDistance, which is just a double, so you can use it in arithmetic calculations, as you’ll see in the application we’re about to create. Here’s how you use this method:

CLLocationDistance distance = [fromLocation distanceFromLocation:toLocation];

The preceding line of code will return the distance between two CLLocation objects: fromLocation and toLocation. This distance value returned will be the result of a great-circle distance calculation that ignores the altitude property and calculates the distance as if both points were at sea level. For most purposes, a great-circle calculation will be more than sufficient; however, if you do want to take altitude into account when calculating distances, you’ll need to write your own code to do it.

Note If you’re not sure what’s meant by great-circle distance, you might want to think back to geography class and the notion of a great-circle route. The idea is that the shortest distance between any two points on the earth’s surface will be found along a path that would, if extended, go the entire way around the earth: a “great circle.” The most obvious great circles are perhaps the ones you’ve seen on maps: the equator and the longitudinal lines. However, such a circle can be found for any two points on the surface of the earth. The calculation performed by CLLocation determines the distance between two points along such a route, taking the curvature of the earth into account. Without accounting for that curvature, you would end up with the length of a straight line connecting the two points, which isn’t much use, since that line would invariably go straight through some amount of the earth itself!

Error Notifications

If Core Location needs to report an error to your application, it will call a delegate method named locationManager:didFailWithError:. One possible cause of an error is that the user denied access to location services, in which case the method will be called with the error codekCLErrorDenied. Another commonly encountered error code supported by the location manager is kCLErrorLocationUnknown, which indicates that Core Location was unable to determine the location but that it will keep trying. While a kCLErrorLocationUnknown error indicates a problem that may be temporary, kCLErrorDenied and other errors may indicate that your application will not be able to access Core Location any time during the remainder of the current session.

Note The simulator has no way to determine your current location, but you can choose one (such as Apple’s HQ, which is the default) or set your own, from the simulator’s Debug image Location menu.

Trying Out Core Location

Let’s build a small application to detect your device’s current location and the total distance traveled while the program has been running. You can see what the first version of our application will look like in Figure 19-2.

image

Figure 19-2. The WhereAmI application in action

In Xcode, create a new project using the Single View Application template and call it WhereAmI. When the project window opens, select ViewController.m and make the following changes:

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

@interface ViewController () <CLLocationManagerDelegate>

@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong, nonatomic) CLLocation *previousPoint;
@property (assign, nonatomic) CLLocationDistance totalMovementDistance;
@property (weak, nonatomic) IBOutlet UILabel *latitudeLabel;
@property (weak, nonatomic) IBOutlet UILabel *longitudeLabel;
@property (weak, nonatomic) IBOutlet UILabel *horizontalAccuracyLabel;
@property (weak, nonatomic) IBOutlet UILabel *altitudeLabel;
@property (weak, nonatomic) IBOutlet UILabel *verticalAccuracyLabel;
@property (weak, nonatomic) IBOutlet UILabel *distanceTraveledLabel;

@end

First, notice that we’ve included the Core Location header file. Core Location is not part of either UIKit or Foundation, so we need to include the header files manually. Next, we conform this class to the CLLocationManagerDelegate method, so that we can receive location information from the location manager.

Then, we declare a CLLocationManager pointer, which will be used to hold a pointer to the instance of the Core Location Manager we’re going create. We also declare a pointer to a CLLocation, which we will set to the location of the last update we received from the location manager. This way, each time the user moves far enough to trigger an update, we’ll be able to add the latest movement distance to our running total, which we’ll keep in the totalMovementDistance property.

The remaining properties are outlets that will be used to update labels on the user interface.

Now select Main.storyboard and let’s start creating the GUI. First, expand the view controller hierarchy in the Document Outline, select the view item and change its name to Main View, and then in the Attributes Inspector, change its background color to light gray. Next, drag a UIView from the object library, drop it onto the existing view, and then position and size it so that it covers the bottom half of the Main View. Make sure that the bottom, left, and right sides of the view exactly match those of the gray view. You are aiming to create something like the arrangement shown inFigure 19-2, where the view that you just dropped is the one at the bottom of the figure with the white background.

In the Document Outline, select the view that you just added and change its name to Lower View. Control-drag from the Lower View to Main View and release the mouse. In the pop-up menu that appears, hold down the Shift key and click Leading Space to Container Margin, Trailing Space to Container Margin, and Bottom Space to Bottom Layout Guide. This pins the Lower View in place, but does not yet set its height. To fix that, with the Lower View still selected in the Document Outline, click the Pin button. In the pop-up, select the Height check box and set the height to166, set Update Frames to Items of New Constraint, and then press Add 1 Constraint to set the height. That should do the job.

Next, we’ll create the rightmost column of labels shown in Figure 19-2. Drag a label from the object library and drop it a little way below the top of the Lower View. Resize it to a width of about 80 points and move it so that it is close to the right edge of the view. Option-drag a copy of this label downward five times to create a stack of labels, as shown in Figure 19-2. Now let’s fix the labels’ sizes and positions relative to the Lower View.

Starting with the topmost label in the Document Outline, Control-drag from that label to Lower View. Release the mouse. Hold down the Shift key and select Top Space to Container Margin and Trailing Space to Container Margin, and then click anywhere else with the mouse to close the pop-up. To set the label’s size, click the Pin button to open the Add New Constraints pop-up menu, click the Width and Height check boxes, enter 80 as the width and 21 as the height (if they are not already set), and click Add 2 Constraints. You have now fixed the size and position of the top label. If you select the label in the storyboard editor, you should see the constraints shown on the left in Figure 19-3. Repeat the same procedure for the other five labels.

image

Figure 19-3. Preparing the labels that will show location information

Next, we’ll add the second column of labels. Drag a label from the object library and place it to the left of the topmost label, leaving a small horizontal gap between them. Drag the left side of the label so that it almost reaches the left edge of Lower View, and then in the Attributes Inspector, set the Alignment so that the label text is right-aligned. Make five copies of this label by Option-dragging downward, aligning each of them with the corresponding label on the right, to make the left column shown on the right in Figure 19-3.

Select the top label in the left column and Control-drag from its left side to the left side of Lower View. Release the mouse and, in the context menu, select Leading Space to Container Margin. Next, Control-drag from the same label to the matching label in the right-hand column. Release the mouse to open the context menu, hold down the Shift key, select Horizontal Spacing and Baseline, and then click outside the context menu with the mouse. Do the same for the other five labels in the left column. Finally, select the View Controller icon in the Document Outline, click the Resolve Auto Layout Issues button, and select Update Frames, if it’s enabled. Any orange warning indications should disappear and the lower part of the screen should look like the right side in Figure 19-3.

We are almost there! We now need to connect the labels in the right column to the outlets in the view controller. Open ViewController.m in the assistant editor and Control-drag from the top label in the right column to the outlet called latitudeLabel in the view controller. Release the mouse to connect the outlet. Control-drag from the second label to longitudeLabel to connect the second outlet, from the third label to horizontalAccuracyLabel, from the fourth to altitudeLabel, from the fifth to verticalAccuracyLabel, and from the bottom label to distanceTraveledLabel. You have now connected all six outlets.

Finally, clear the text from all of the labels in the right column and change the text of the labels in the left column to match that shown in Figure 19-2; the top label’s text should be Latitude:, the next one down should be Longitude:, and so on.

Now let’s write the code to display some useful information in all those labels. Select ViewController.m and insert the following lines in viewDidLoad to configure the location manager:

- (void)viewDidLoad
{
[super viewDidLoad];

self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[self.locationManager requestWhenInUseAuthorization];
}

We allocate and initialize a CLLocationManager instance, assign our controller class as the delegate, set the desired accuracy to the best available, and then request permission to use the location service while the user is using our application. This is sufficient authorization for the purposes of this example. To use some of the more advanced features of Core Location, which are beyond the scope of this book, you will probably need to request permission to use Core Location at any time by calling the requestAlwaysAuthorization method instead.

Note In this simple example, the request for authorization is made as the application starts up, but Apple recommends that, in a real application, you should delay making the request until you actually need to use location services. The reason for this is that the user is more likely to agree if it’s obvious why you need access to the device’s location, based on operation that has been requested, than if an application, probably one that the user has just installed, requests permission as soon as it launches.

The first time this application runs, iOS will display an alert asking the user whether your application should be allowed to use your location. You need to supply a short piece of text that iOS will include in the alert pop-up, explaining why your application needs to know the user’s location. Open the info.plist file and add the text you’d like to have displayed under the key NSLocationWhenInUseUsageDescription (if you need to request permission to use location services even when the application is not actively being used, the text should be added under the keyNSLocationAlwaysUsageDescription instead). For the purposes of this example, use something like “The application needs to know your location to update your position on a map”.

Caution In earlier versions of iOS, supplying text to qualify the permission request was optional. As of iOS 8, it is mandatory. If you don’t supply any text, the permission request will not be made.

If you run the application now, you’ll see that iOS uses your text in the permission request, as shown in Figure 19-4.

image

Figure 19-4. Prompting the user for permission to use location services

This prompt appears only once in the lifetime of the application. Whether or not the user allows your application to use location services, this request will never be made again, no matter how many times the application is run. That’s not to say that the user can’t change his mind about this, of course. We’ll say more about that in the upcoming “Changing Location Service Permissions” section. As far as testing is concerned, rerunning the application from Xcode has no effect on the user’s saved response—to get a clean state for testing, you have to delete the application from the simulator or device. If you do that, iOS will prompt for permission again when you reinstall and relaunch the application. For now, reply “Allow” to the prompt and let’s continue writing our application.

You probably noticed that the viewDidLoad method did not call the location manager’s startUpdatingLocation method immediately after calling requestWhenInUseAuthorization. There is, in fact, no point in doing so, because the authorization process does not take place immediately. At some point after viewDidLoad returns, the location manager delegate’s locationManager:didChangeAuthorizationStatus: method will be called with the application’s authorization status. This may be the result of the user’s reply to the permission request pop-up, or it may be the saved authorization state from when the application last executed. Either way, this method is an ideal place to start listening for location updates, assuming you are authorized to. Add the following implementation of this method to the ViewController.m file:

- (void)locationManager:(CLLocationManager *)manager
didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
NSLog(@"Authorization status changed to %d", status);
switch (status) {
case kCLAuthorizationStatusAuthorizedAlways:
case kCLAuthorizationStatusAuthorizedWhenInUse:
[self.locationManager startUpdatingLocation];
break;

case kCLAuthorizationStatusNotDetermined:
case kCLAuthorizationStatusRestricted:
case kCLAuthorizationStatusDenied:
[self.locationManager stopUpdatingLocation];
break;
}
}

This code starts listening for location updates if authorization was granted, and stops listening if it was not. Since we don’t start listening unless we have authorization, what’s the point of calling stopUpdatingLocation if we didn’t get permission? That’s a good question. The reason this code is required is because the user can give your application permission to use Core Location and then later revoke it. In that case, we need to stop listening for updates. For more on this, see “Changing Location Service Permissions” later in this chapter.

If your application tries to use location services when it doesn’t have permission to do so, or if an error occurs at any time, the location manager calls its delegate’s locationManager:didFailWithError: method. Let’s add an implementation of that method to the view controller:

- (void)locationManager:(CLLocationManager *)manager
didFailWithError:(NSError *)error {
NSString *errorType = error.code == kCLErrorDenied ? @"Access Denied"
: [NSString stringWithFormat:@"Error %ld", (long)error.code, nil];
UIAlertController *alertController =
[UIAlertController alertControllerWithTitle:@"Location Manager Error"
message:errorType
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleCancel handler:nil];
[alertController addAction:okAction];
[self presentViewController:alertController animated:YES completion:nil];
}

For the purposes of this example, when an error occurs, we just alert the user. In a real application, you would use a more meaningful error message and clean up the application state as required.

Using Location Manager Updates

Now that we’ve dealt with getting permission to use the user’s location, let’s do something with that information. Insert this implementation of the delegate’s locationManager:didUpdateLocation: method at the end of the @implementation block:

- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations {
CLLocation *newLocation = [locations lastObject];
NSString *latitudeString = [NSString stringWithFormat:@"%g\u00B0",
newLocation.coordinate.latitude];
self.latitudeLabel.text = latitudeString;

NSString *longitudeString = [NSString stringWithFormat:@"%g\u00B0",
newLocation.coordinate.longitude];
self.longitudeLabel.text = longitudeString;

NSString *horizontalAccuracyString = [NSString stringWithFormat:@"%gm",
newLocation.horizontalAccuracy];
self.horizontalAccuracyLabel.text = horizontalAccuracyString;

NSString *altitudeString = [NSString stringWithFormat:@"%gm",
newLocation.altitude];
self.altitudeLabel.text = altitudeString;

NSString *verticalAccuracyString = [NSString stringWithFormat:@"%gm",
newLocation.verticalAccuracy];
self.verticalAccuracyLabel.text = verticalAccuracyString;

if (newLocation.verticalAccuracy < 0 ||
newLocation.horizontalAccuracy < 0) {
// invalid accuracy
return;
}

if (newLocation.horizontalAccuracy > 100 ||
newLocation.verticalAccuracy > 50) {
// accuracy radius is so large, we don't want to use it
return;
}

if (self.previousPoint == nil) {
self.totalMovementDistance = 0;
} else {
NSLog(@"movement distance: %f",
[newLocation distanceFromLocation:self.previousPoint]);
self.totalMovementDistance +=
[newLocation distanceFromLocation:self.previousPoint];
}
self.previousPoint = newLocation;

NSString *distanceString = [NSString stringWithFormat:@"%gm",
self.totalMovementDistance];
self.distanceTraveledLabel.text = distanceString;

}

The first thing we do in the delegate method is to update the first five labels in the second column of Figure 19-2 with values from the CLLocation objects passed in the locations argument. The locations array could contain more than one location update, but use the last entry, which always represents the most recent information.

Note Both the longitude and latitude are displayed in formatting strings containing the cryptic-looking \u00B0. This is the hexadecimal value of the Unicode representation of the degree symbol (°). It’s never a good idea to put anything other than ASCII characters directly in a source code file, but including the hex value in a string is just fine, and that’s what we’ve done here.

Next, we check the accuracy of the values that the location manager gives us. High accuracy values indicate that the location manager isn’t quite sure about the location, while negative accuracy values indicate that the location is actually invalid. However, some devices do not have the hardware required to determine vertical position. On these devices, and on the simulator, the verticalAccuracy property will always be –1, so we don’t exclude position reports that have this value.

These accuracy values are in meters and indicate the radius of a circle from the location we’re given, meaning that the true location could be anywhere in that circle. Our code checks to see whether these values are acceptably accurate; if not, it simply returns from this method rather than doing anything more with garbage data:

if (newLocation.horizontalAccuracy < 0) {
// invalid accuracy
return;
}

if (newLocation.horizontalAccuracy > 100 ||
newLocation.verticalAccuracy > 50) {
// accuracy radius is so large, we don't want to use it
return;
}

Next, we check whether previousPoint is nil. If it is, then this update is the first valid one we’ve gotten from the location manager, so we zero out the distanceFromStart property. Otherwise, we add the latest location’s distance from the previous point to the total distance. In either case, we update previousPoint to contain the current location:

if (self.previousPoint == nil) {
self.totalMovementDistance = 0;
} else {
self.totalMovementDistance += [newLocation
distanceFromLocation:self.previousPoint];
}
self.previousPoint = newLocation;

After that, we populate the final label with the total distance that we’ve traveled from the start point. While this application runs, if the user moves far enough for the location manager to detect the change, the Distance Traveled: field will be continually updated with the distance the user has moved since the application started:

NSString *distanceString = [NSString stringWithFormat:@"%gm",
self.totalMovementDistance];
self.distanceTraveledLabel.text = distanceString;

And there you have it. Core Location is fairly straightforward and easy to use.

Compile and run the application, and then try it. If you have the ability to run the application on your iPhone or iPad, try going for a drive with the application running, and watch the values change as you drive. Um, actually, it’s better to have someone else do the driving!

Visualizing Your Movement on a Map

What we’ve done so far is pretty neat, but wouldn’t it be nice if we could visualize our travel on a map? Fortunately, iOS includes the Map Kit framework to help us out here. Map Kit utilizes the same back-end services that Apple’s Maps app uses, which means it’s fairly robust and improving all the time. It contains a view class that presents a map, and it responds to user gestures just as you’d expect of any modern mapping app. This view also lets us insert annotations for any locations we want to show up on our map, which by default show up as “pins” that can be touched to reveal some more info. We’re going to extend our WhereAmI app to display the user’s starting position and current position on a map.

Select ViewController.m and add the following near the top to import the Map Kit framework headers:

#import <MapKit/MapKit.h>

Now add a new property declaration for the Map View that will display the user’s location below the others in the class extension:

@property (weak, nonatomic) IBOutlet MKMapView *mapView;

Now select Main.storyboard to edit the view. Drag a Map View from the object library and drop it onto the user interface. Resize the Map View so that it covers the whole screen, including the Lower View and all of its labels, and then choose Editor image Arrange image Send to Back to move the Map View behind Lower View. In the Document Outline, Control-drag from the Map View to the Main View and, in the context menu, hold down the Shift key and select Leading Space to Container Margin, Trailing Space to Container Margin, Top Space to Top Layout Guide, and Bottom Space to Bottom Layout Guide, and then click outside the context menu with the mouse.

The Map View is now locked in place, but the bottom part of it is obscured. We can fix that by making Lower View partly transparent. To do that, select Lower View in the Document Outline, open the Attributes Inspector, click the Background color editor and, in the pop-up that appears, choose Other… to open a color chooser. Select a white background and move the Opacity slider to about 70%. Finally, Control-drag from the Map View to the mapView property in ViewController.m to connect the outlet.

Now that these preliminaries are in place, it’s time to write a little code that will make the map do some work for us. Before dealing with the code required in the view controller, we need to set up a sort of model class to represent our starting point. MKMapView is built as the View part of an MVC (Model-View-Controller) architecture, and it works best if we have distinct classes to represent markers on the map. We can pass model objects off to the map view, and it will query them for coordinates, a title, and so on, using a protocol defined in the Map Kit framework.

Press imageN to bring up the new file assistant, and in the iOS section, choose Cocoa Touch Class. Name the class Place and make it a subclass of NSObject. Select Place.h and modify it as shown next. You need to import the Map Kit header, specify a protocol that the new class conforms to, and add some properties:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface Place : NSObject <MKAnnotation>

@property (copy, nonatomic) NSString *title;
@property (copy, nonatomic) NSString *subtitle;
@property (assign, nonatomic) CLLocationCoordinate2D coordinate;

@end

This is a fairly “dumb” class that acts solely as a holder for these properties. We don’t even need to touch the .m file here! In a real-world example, you may have real model classes that need to be shown on a map as an annotation, and the MKAnnotation protocol lets you add this capability to any class of your own without messing up any existing class hierarchies.

Select ViewController.m and get started by importing the header for the new class:

#import "Place.h"

Now add the following two lines to the locationManager:didChangeAuthorizationStatus: method:

- (void)locationManager:(CLLocationManager *)manager
didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
NSLog(@"Authorization status changed to %d", status);
switch (status) {
case kCLAuthorizationStatusAuthorizedAlways:
case kCLAuthorizationStatusAuthorizedWhenInUse:
[self.locationManager startUpdatingLocation];
self.mapView.showsUserLocation = YES;
break;

case kCLAuthorizationStatusNotDetermined:
case kCLAuthorizationStatusRestricted:
case kCLAuthorizationStatusDenied:
[self.locationManager stopUpdatingLocation];
self.mapView.showsUserLocation = NO;
break;
}
}

The Map View’s showsUserLocation property does just what you probably imagine: it saves us the hassle of manually moving a marker around as the user moves by automatically drawing one for us. It uses Core Location to get the user’s location and it works only if your application is authorized for that, so we enable the property when we are told that we have permission to use Core Location, and disable it again if we lose permission.

Now let’s revisit the locationManager:didUpdateLocations: method. We’ve already got some code in there that notices the first valid location data we receive and establishes our start point. We’re also going to allocate a new instance of our Place class. We set its properties, giving it a location. We also add a title and subtitle that we want to appear when a marker for this location is displayed. Finally, we pass this object off to the map view.

We also create an instance of MKCoordinateRegion, a struct included in Map Kit that lets us tell the view which section of the map we want it to display. MKCoordinateRegion uses our new location’s coordinates and a pair of distances in meters (100, 100) that specify how wide and tall the displayed map portion should be. We pass this off to the map view as well, telling it to animate the change. All of this is done by adding the bold lines shown here:

if (self.previousPoint == nil) {
self.totalMovementDistance = 0;

Place *start = [[Place alloc] init];
start.coordinate = newLocation.coordinate;
start.title = @"Start Point";
start.subtitle = @"This is where we started!";

[self.mapView addAnnotation:start];
MKCoordinateRegion region;
region = MKCoordinateRegionMakeWithDistance(newLocation.coordinate,
100, 100);
[self.mapView setRegion:region animated:YES];
} else {
self.totalMovementDistance += [newLocation
distanceFromLocation:self.previousPoint];
}
self.previousPoint = newLocation;

So now we’ve told the map view that we have an annotation (i.e., a visible placemark) that we want the user to see. But how should it be displayed? Well, the map view figures out what sort of view to display for each annotation by asking its delegate. In a more complex app, that would work for us. But in this example we haven’t made ourselves a delegate, simply because it’s not necessary for our simple use case. Unlike UITableView, which requires its data source to supply cells for display, MKMapView has a different strategy: if it’s not provided with annotation views by a delegate, it simply displays a default sort of view represented by a red “pin” on the map that reveals some more information when touched. Neat!

There’s one final thing you need to do—enable your application to use Map Kit. To do this, select the project in the Project Navigator and then select the WhereAmI target. At the top of editor area, select Capabilities, locate the Maps section, and move the selector switch on the right from OFF to ON. Now build and run your app, and you’ll see the map view load. As soon as it gets valid position data, you’ll see it scroll to the right location, drop a pin at your starting point, and mark your current location with a glowing blue dot (see Figure 19-5). Not bad for a few dozen lines of code!

image

Figure 19-5. The red pin marks our starting location, and the blue dot shows how far we’ve gotten—in this case, no distance at all!

Changing Location Service Permissions

When your application runs for the first time, you hope the user will give it permission to use location services. Whether you get permission or not, you can’t assume that nothing will change. The user can grant or revoke location permission via the Settings app. You can test this on the simulator. Launch the app and grant yourself permission to use Core Location (if you’ve previously denied permission, you’ll need to remove and reinstall the app first). You should see your location on the map. Now go to the Settings app and choose Privacy image Location. At the top of the screen is a switch that turns location services on or off. Turn the switch to OFF and go back to your application. You’ll see that the map no longer shows your position. That’s because the location manager called the locationManager:didChangeAuthorizationStatus: methodwith authorization code kCLAuthorizationStatusDenied, in response to which the application stops receiving position updates and tells Map Kit to stop tracking the user’s position. Now go back to the Settings app, re-enable Core Location, and come back to your application; you’ll find that it’s tracking your position again.

Switching Location Services off is not the only way for the user to change your app’s ability to use Core Location. Go back to the Settings app. Below the switch that enables Location Services, you’ll see a list of all the apps that are using it, including WhereAmI, as shown on the left inFigure 19-6. Clicking the application name takes you to another page where you can allow or deny access to your application, which you can see on the right in Figure 19-6. At the moment, the application can use location services while the user is using the app. If you click Never, that permission is revoked, as you can prove by returning to the application again. This demonstrates that it’s important to code the application so that it can detect and respond properly to changes in its authorization status.

image

Figure 19-6. Changing Core Location access permission for the WhereAmI app

Wherever You Go, There You Are

That’s the end of our introduction to Core Location and Map Kit. There is quite a lot more to be discovered about both of these frameworks. Here are just a few of the highlights:

· Instead of closely tracking the user’s location using the startUpdatingLocation method, applications that need less positional accuracy and/or less frequent updates, such as Weather apps, can use the Significant Location Updates service. You should use this service if at all possible, because it can significantly reduce power consumption.

· On devices that have a magnetometer, Core Location can report the user’s heading. If the device also has a GPS, it can report the direction in which the user is moving.

· Core Location can report when the user enters or leaves application-defined geographical regions (defined as a circle of a given radius and center) or when the application is in the vicinity of an iBeacon.

· You can convert between the coordinates reported by Core Location and a user-friendly placemark object and vice versa, using the Geocoding service. In addition to this, Map Kit includes an API that lets you search for locations by name or address.

· New in iOS 8, Core Location monitors the user’s movement and can determine when the user stops for a period of time at a location. When this happens, the user is assumed to be “visiting” that location. Your application can receive notification when the user arrives at and departs from a visited location.

The best source of information for all of these features is Apple’s Location and Maps Programming Guide.

Although the underlying technologies are quite complex, Apple has provided simple interfaces that hide most of the complexity, making it quite easy to add location-related and mapping features to your applications so that you can tell where the users are, notice when they move, and mark their location (and any other locations) on a map.

And speaking of moving, when you’re ready, proceed directly to the next chapter so that we can play with the iPhone’s built-in accelerometer.