Hour 21. Implementing Location Services


What You’ll Learn in This Hour:

Image The available iOS location-sensing hardware

Image How to read and display location information

Image Detecting orientation with the compass


In the preceding hour’s lesson, we looked briefly at the use of Map Kit to display map information in an application. In this lesson, we take the GPS capabilities of our devices a step further: We tie into the hardware capabilities of the iDevice lineup to accurately read location data and compass information.

In this hour, we work with Core Location and the electromagnetic compass. With location-enabled apps enhancing the user experience in areas such as Internet searches, gaming, and even productivity, you can add value and interest to your own offerings with these tools.

Understanding Core Location

Core Location is a framework in the iOS SDK that provides the location of the device. Depending on the device and its current state (within cell service, inside a building, and so forth), any of three technologies can be used: GPS, cellular, or WiFi. GPS is the most accurate of these technologies and will be used first by Core Location if GPS hardware is present. If the device does not have GPS hardware (WiFi iPads, for example), or if obtaining the current location with GPS fails, Core Location falls back to cellular and then to WiFi.

Getting Locations

Core Location is simple to understand and to use despite the powerful array of technologies behind it. (Some of it had to be launched into space on rockets.) Most of the functionality of Core Location is available from the location manager, which is an instance of the CLLocationManager class. You use the location manager to specify the frequency and accuracy of the location updates you are looking for and to turn on and off receiving those updates.

To use a location manager, you must first import the Core Location framework to your project. This can be done with a single statement:

import CoreLocation

Next, you initialize an instance of the location manager, specify a delegate that will receive location updates, ask the user for permission to use her location (more on that in a second), and then start the updating, like this:

let locMan: CLLocationManager = CLLocationManager()
locMan.delegate = self
locMan.requestWhenInUseAuthorization()
locMan.startUpdatingLocation()

When the application has finished receiving updates (a single update is often sufficient), stop location updates with location manager’s stopUpdatingLocation method.

Requesting Authorization and the Plist File

As you learned in the preceding hour, previous versions of iOS (before 8) automatically asked for permission to use a user’s location. In iOS 9, this (as you can seen in the previous code snippet) is now an explicit action. You must make sure your code calls either requestWhenInUseAuthorization or requestAlwaysAuthorization before attempting to use the location. The former authorizes your app to use the location only when it is in the foreground and active, whereas the latter asks for location information to be available all the time. Apple advises against using requestAlwaysAuthorization unless you have a really good reason to track location in the background.

In addition to making the request for permission, you must also add one of two keys to your application’s Info.plist file (in the Supporting Files group): NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription. These keys should be set with a string value with a message you’d like displayed to the user when asking for permission to use their location.

If you leave these methods or plist keys out of your project, it will likely sit there and do absolutely nothing. This is a big change over earlier versions of the system, so don’t rush your code!

Location Manager Delegate

The location manager delegate protocol defines the methods for receiving location updates. Whatever class we’ve designated as our delegate for receiving location updates must conform to the CLLocationManagerDelegate protocol.

Two methods in the delegate relate to location: locationManager:didUpdateLocations and locationManager:didFailWithError.

The locationManager:didUpdateToLocations method’s argument are the location manager object and an array CLLocation objects. (Multiple locations may be returned depending on how quickly you’re moving.) A CLLocation instance provides a coordinate variable property that is a structure containing longitude and latitude expressed in CLLocationDegrees. CLLocationDegrees is just an alias for a floating-point number of type double.

As already mentioned, different approaches to geolocating have different inherit accuracies, and each approach may be more or less accurate depending on the number of points (satellites, cell towers, WiFi hot spots) it has available to use in its calculations. CLLocation passes this confidence measure along in the horizontalAccuracy variable property.

The location’s accuracy is provided as a circle, and the true location could lie anywhere within that circle. The circle is defined by the coordinate variable property as the center of the circle and the horizontalAccuracy as the radius of the circle in meters. The larger the horizontalAccuracy, the larger the circle defined by it will be, so the less confidence there is in the accuracy of the location. A negative horizontalAccuracy indicates that the coordinate is completely invalid and should be ignored.

In addition to longitude and latitude, each CLLocation provides altitude (in meters) above or below sea level. The altitude property is a CLLocationDistance, which is, again, just a floating-point number. A positive number is an altitude above sea level, and a negative number is below sea level. Another confidence factor, this one called verticalAccuracy, indicates how accurate the altitude is. A positive verticalAccuracy indicates that the altitude could be off, plus or minus, by that many meters. A negative verticalAccuracy means the altitude is invalid.

Listing 21.1 shows an implementation of the location manager delegate’s locationManager:didUpdateToLocations method that logs the longitude, latitude, and altitude.

LISTING 21.1 Implementing Location Updates


 1: func locationManager(manager: CLLocationManager,
 2:         didUpdateLocations locations: [CLLocation]) {
 3:     let newLocation: CLLocat1ion=locations[0]
 4:
 5:     var coordinateDesc:String = "Not Available"
 6:     var altitudeDesc:String = "Not Available"
 7:
 8:     if newLocation.horizontalAccuracy >= 0 {
 9:       coordinateDesc =
10:       "(newLocation.coordinate.latitude), (newLocation.coordinate.longitude)"
11:       coordinateDesc = coordinateDesc +
12:           " +/- (newLocation.horizontalAccuracy) meters"
13:     }
14:
15:     if newLocation.verticalAccuracy >= 0 {
16:         altitudeDesc = "(newLocation.altitude)"
17:         altitudeDesc = altitudeDesc +
18:             " +/- (newLocation.verticalAccuracy) meters"
19:     }
20:
21:     NSLog("Lat/Long: (coordinateDesc)   Altitude: (altitudeDesc)")
22: }


The key statements to pay attention to in this implementation are the references to the accuracy measurements in lines 8 and 15, and accessing the latitude, longitude, and altitude in lines 9–12, and 16–18. These are just variable properties, something you’ve grown accustomed to working with over the past 20 hours.

One element in this example that you might not be familiar with is line 21’s NSLog function. NSLog, which you learn to use in Hour 24, “Application Tracing, Monitoring, and Debugging,” provides a convenient way to output information (often debugging information) without having to design a view.

The resulting output looks like this:

Lat/Long: 35.904392, -79.055735 +/- 76.356886 meters
Altitude: 28.000000 +/- 113.175757 meters


Caution: Watch Your Speed

CLLocation also provides a variable property speed, which is based on comparing the current location with the prior location and comparing the time and distance variance between them. Given the rate at which Core Location updates, speed is not very accurate unless the rate of travel is fairly constant.


Handling Location Errors

As I wrote earlier, before using a user’s location, you must prompt for permission (this only occurs once, if the user accepts), as shown in Figure 21.1.

Image

FIGURE 21.1 Core Location asks permission to provide an application with location data.

If the user chooses to disallow location services, iOS does not prevent your application from running, but instead generates errors from the location manager.

When an error occurs, the location manager delegate’s locationManager:didFailWithError method is called, letting you know the device cannot return location updates. A distinction is made as to the cause of the failure. If the user denies permission to the application, the error argument is CLError.Denied; if Core Location tries but cannot determine the location, the error is CLError.LocationUnknown; and if no source of trying to retrieve the location is available, the error is CLError.Network. Usually Core Location continues to try to determine the location after an error. After a user denial, however, it doesn’t, and it is good form to stop the location manager with location manager’s stopUpdatingLocation method. Listing 21.2 shows a simple implementation of locationManager:didFailWithError.

LISTING 21.2 Reacting to Core Location Errors


 1: func locationManager(manager: CLLocationManager,
 2:         didFailWithError error: NSError) {
 3:     if error.code == CLError.Denied.rawValue {
 4:         NSLog("Permission to retrieve location is denied.")
 5:         locMan.stopUpdatingLocation()
 6:     } else if error.code==CLError.Network.rawValue {
 7:         NSLog("Network used to retrieve location is unavailable.")
 8:     } else if error.code==CLError.LocationUnknown.rawValue {
 9:         NSLog("Currently unable to retrieve location.")
10:     }
11: }


As with the previous example implementation of handling location manager updates, in the error handler we also work solely with the objects the method receives. In lines 3, 6, and 8, we check the incoming NSError object’s code variable property against the possible error conditions and react accordingly. Note that in order to make the comparison, we must add rawValue to the end of each core location error constant.


Caution: Please Wait While I Get My Bearings

Keep in mind that the location manager delegate will not immediately receive a location. It usually takes a number of seconds for the device to pinpoint the location, and the first time it is used by an application, Core Location first asks the user’s permission. You should have a design in place for what the application will do while waiting for an initial location and what to do if location information is unavailable because the user didn’t grant permission or the geolocation process failed. A common strategy that works for many applications is to fall back to a user-entered ZIP code.


Location Accuracy and Update Filter

It is possible to tailor the accuracy of the location to the needs of the application. An application that needs only the user’s country, for example, does not need 10-meter accuracy from Core Location and will get a much faster answer by asking for a more approximate location. You do this by setting the location manager’s desiredAccuracy variable property, before you start the location updates. desiredAccuracy is an enumerated type, CLLocationAccuracy. Six constants are available with varying levels of precision (with current consumer technology, the first two are the same): kCLLocationAccuracyBest, kCLLocationAccuracyNearestTenMeters, kCLLocationAccuracyBestForNavigation, kCLLocationAccuracyNearestHundredMeters, kCLLocationAccuracyKilometer, and kCLLocationAccuracyThreeKilometers.

After updates on a location manager are started, updates continue to come into the location manager delegate until they are stopped. You cannot control the frequency of these updates directly, but you can control it indirectly with location manager’s distanceFilter variable property. distanceFilter is set before starting updates and specifies the distance in meters the device must travel (horizontally, not vertically) before another update is sent to the delegate.

For example, starting the location manager with settings suitable for following a walker’s progress on a long hike might look like this:

let locMan: CLLocationManager = CLLocationManager()
locMan.delegate = self
locMan.desiredAccuracy = kCLLocationAccuracyHundredMeters
locMan.distanceFilter = 200
locMan.requestWhenInUseAuthorization()
locMan.startUpdatingLocation()


Caution: Location Comes with a Cost

Each of the three methods of locating the device (GPS, cellular, and WiFi) can put a serious drain on the device’s battery. The more accurate an application asks the device to be in determining location, and the shorter the distance filter, the more battery the application will use. Be aware of the device’s battery life and only request as accurate and as frequent location updates as the application needs. Stop location manager updates whenever possible to preserve the battery life of the device.


Getting Headings

The location manager includes a headingAvailable variable property that indicates whether the device is equipped with a magnetic compass. If the value is YES, you can use Core Location to retrieve heading information. Receiving heading events works similarly to receiving location update events. To start receiving heading events, assign a location manager delegate, assign the headingFilter variable property for how often you want to receive updates (measured in degrees of change in heading), and call the startUpdatingHeading method on the location manager object:

locMan.delegate=self
locMan.headingFilter=10
locMan.startUpdatingHeading()


Caution: North Isn’t Just “Up”

There isn’t one true north. Geographic north is fixed at the North Pole, and magnetic north is located hundreds of miles away and moves every day. A magnetic compass always points to magnetic north; but some electronic compasses, like the one in the iPhone and iPad, can be programmed to point to geographic north instead. Usually, when we deal with maps and compasses together, geographic north is more useful. Make sure that you understand the difference between geographic and magnetic north and know which one you need for your application. If you are going to use the heading relative to geographic north (the trueHeading variable property), request location updates as well as heading updates from the location manager; otherwise, the trueHeading won’t be properly set.


The location manager delegate protocol defines the methods for receiving heading updates. Two methods in the delegate relate to headings: locationManager:didUpdateHeading and locationManagerShouldDisplayHeadingCalibration.

The locationManager:didUpdateHeading method’s argument is a CLHeading object. The CLHeading object makes the heading reading available with a set of variable properties: the magneticHeading and the trueHeading. (See the relevant Caution.) These values are in degrees, and are of type CLLocationDirection, which is a floating-point number. In plain English, this means that

Image If the heading is 0.0, we’re going north.

Image When the heading reads 90.0, we’re headed due east.

Image If the heading is 180.0, we’re going south.

Image Finally, if the heading reads 270.0, we’re going west.

The CLHeading object also contains a headingAccuracy confidence measure, a timestamp of when the reading occurred, and an English language description that is more suitable for logging than showing to a user. Listing 21.3 shows an implementation example of the locationManager:didUpdateHeading method.

LISTING 21.3 Handling Heading Updates


 1:  func locationManager(manager: CLLocationManager,
 2:         didUpdateHeading newHeading: CLHeading) {
 3:      var headingDesc: String = "Not Available."
 4:      if newHeading.headingAccuracy >= 0 {
 5:          let trueHeading: CLLocationDirection =
 6:              newHeading.trueHeading
 7:          let magneticHeading: CLLocationDirection  =
 8:              newHeading.magneticHeading
 9:          headingDesc =
10:  "(trueHeading) degrees (true), (magneticHeading) degrees (magnetic)"
11:      }
12:      NSLog(headingDesc)
13:  }


This implementation looks very similar to handling location updates. We check to make sure there is valid data (line 4), and then we grab the true and magnetic headings from the trueHeading and magneticHeading variable properties passed to us in the CLHeading object (lines 5–8). The output generated looks a bit like this:

180.9564392 degrees (true), 182.684822 degrees (magnetic)

The other delegate method, locationManager:ShouldDisplayHeadingCalibration, literally consists of a line returning true or false. This indicates whether the location manager can display a calibration prompt to the user. The prompt asks the user to step away from any source of interference and to rotate the device 360 degrees. The compass is always self-calibrating, and this prompt is just to help that process along after the compass receives wildly fluctuating readings. It is reasonable to implement this method to return false if the calibration prompt would be annoying or distracting to the user at that point in the application (in the middle of data entry or game play, for example).


Note

The iOS Simulator reports that headings are available, and it provides just one heading update.


Creating a Location-Aware Application

Many iOS and Mac users have a, shall we say, “heightened” interest in Apple Computer; visiting Apple’s campus in Cupertino, California, can be a life-changing experience. For these special users, we’re going to create a Core Location-powered application that keeps you informed of just how far away you are.

Implementation Overview

The application is created in two parts: The first introduces Core Location and displays the number of miles from the current location to Cupertino. In the second section, we use the device’s compass to display an arrow that points users in the right direction, should they get off track.

In this first installment, we create an instance of a location manager and then use its methods to calculate the distance between our current location and Cupertino, California. While the distance is being determined, we display a Please Wait message. In cases where we happen to be in Cupertino, we congratulate the user. Otherwise, a display of the distance, in miles, is shown.

Setting Up the Project

For the rest of this hour, we work on a new application that uses the Core Location framework. Create a new single-view iOS application in Xcode and call it Cupertino.

Adding Background Image Resources

To ensure that the user remembers where we’re going, we have a nice picture of an apple as the application’s background image. Click the main Assets.xcassets asset catalog to open the project’s image assets. Now, within the Finder, drag the project’s Images folder into the left column of the asset catalog in Xcode. The new addition to the asset catalog contains apple.png, our background image, along with several other images we’ll be using later.

Planning the Variables and Connections

The view controller serves as the location manager delegate, receiving location updates and updating the user interface to reflect the new locations. Within the view controller, we need a constant or variable property for an instance of the location manager. We will name this locMan.

Within the interface itself, we need a label with the distance to Cupertino (distanceLabel) and two subviews (distanceView and waitView). The distanceView contains the distanceLabel and is shown only after we’ve collected our location data and completed our calculations. The waitView is shown while our iDevice gets its bearings.

Adding Location Constants

To calculate the distance to Cupertino, we obviously need a location in Cupertino that we can compare to the user’s current location. According to http://gpsvisualizer.com/geocode using Google Maps as the source, the center of Cupertino, California, is at 37.3229978 latitude, –122.0321823 longitude. Add two constants for these values (kCupertinoLatitude and kCupertinoLongitude) after the class line in the ViewController.swift file:

let kCupertinoLatitude: CLLocationDegrees = 37.3229978
let kCupertinoLongitude: CLLocationDegrees = -122.0321823

You can ignore the errors that appear regarding the unknown type CLLocationDegrees. This is a data type that will be defined for us in a few minutes when we include the Core Location framework.

Designing the View

The user interface for this hour’s lesson is simple: We can’t perform any actions to change our location (teleportation isn’t yet possible), so all we need to do is update the screen to show information about where we are.

Open the Main.storyboard file and use the Attributes Inspector to switch to a “normal” simulated screen size. Next, open the Object Library (View, Utilities, Show Object Library), and commence design:

1. Start by adding an image view (UIImageView) onto the view and position it so that it covers the entire view. This serves as the background image for the application.

2. With the image view selected, open the Attributes Inspector (Option-Command-4). Select apple from the Image drop-down menu.

3. Drag a new view (UIView) on top of the image view. Size it to fit in the bottom of the view; it serves as our primary information readout, so it needs to be sized to hold about two lines of text.

4. Use the Attributes Inspector to set the background to black. Change the Alpha to 0.75, and check the Hidden check box.

5. Add a label (UILabel) to the information view. Size the label up to all four edge guidelines and change the text to read Lots of miles to the Mothership. Use the Attributes Inspector to change the text color to white, aligned center, and sized as you want. Figure 21.2 shows my view.

Image

FIGURE 21.2 The beginnings of the Cupertino Locator UI.

6. Create a second semitransparent view with the same attributes as the first, but not hidden, and with a height of about an inch.

7. Drag the second view to vertically center it on the background. This view will contain the Please Wait message while the device is finding our location.

8. Add a new label to the view that reads Checking the Distance. Resize the label so that it takes up approximately the right two-thirds of the view.

9. Drag an activity indicator (UIActivityIndicatorView) to the new view and align it to the left side of the label. The indicator shows a “spinner” graphic to go along with our Checking the Distance label. Use the Attributes Inspector to set the Animated attribute; it makes the spinner spin.

The final view should resemble Figure 21.3.

Image

FIGURE 21.3 The final Cupertino Locator UI.

Creating and Connecting the Outlets

In this exercise, all we do is update the user interface (UI) based on information from the location manager. In other words, there are no actions to connect (hurray). We need connections from the two views we added as well as the label for displaying the distance to Cupertino.

Switch to the assistant editor. Control-drag from the Lots of Miles label to below the class line in ViewController.swift. Create a new outlet named distanceLabel when prompted. Do the same for the two views, connecting the view with the activity indicator to a waitView outlet and the view that contains the distance estimate to a distanceView outlet.

Implementing the Application Logic

Based on the interface we just laid out, the application starts up with a message and a spinner that let the user know that we are waiting on the initial location reading from Core Location. We’ll request this reading as soon as the view loads in the view controller’s viewDidLoad method. When the location manager delegate gets a reading, we calculate the distance to Cupertino, update the label, hide the activity indicator view, and unhide the distance view.

Preparing the Location Manager

To use Core Location and create a location manager, we need to make a few changes to our setup to accommodate the framework. First, update ViewController.swift by importing the Core Location module, and then add the CLLocationManagerDelegate protocol to the class line. The top of ViewController.swift file should now look at bit like this:

import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {

Our project is now prepared to use the location manager, but we still need to add and initialize a constant to use it (locMan).

Update the code at the top of your ViewController.swift file one last time, so that it reads as follows:

import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

    @IBOutlet weak var waitView: UIView!
    @IBOutlet weak var distanceView: UIView!
    @IBOutlet weak var distanceLabel: UILabel!

    let locMan: CLLocationManager = CLLocationManager()

    let kCupertinoLatitude: CLLocationDegrees = 37.3229978
    let kCupertinoLongitude: CLLocationDegrees = -122.0321823

It’s time to configure the location manager and distance calculation code.

Configuring the Location Manager Instance

To use the location manager, we must first configure it. Update the viewDidLoad method in ViewController.swift, and set self as the delegate, a desiredAccuracy of kCLLocationAccuracyThreeKilometers, and a distanceFilter of 1,609 meters (1 mile). Request permission to use the location with requestWhenInUseAuthorization, and then start the updates with the startUpdatingLocation method. The implementation should resemble Listing 21.4.

LISTING 21.4 Creating the Location Manager Instance


override func viewDidLoad() {
    super.viewDidLoad()
    locMan.delegate = self
    locMan.desiredAccuracy = kCLLocationAccuracyThreeKilometers
    locMan.distanceFilter = 1609; // a mile
    locMan.requestWhenInUseAuthorization()
    locMan.startUpdatingLocation()
}


If you have any questions about this code, refer to the introduction to location manager at the start of this hour’s lesson. This code mirrors the examples from earlier, with some slightly changed numbers.

Implementing the Location Manager Delegate

Now we need to implement the two methods of the location manager delegate protocol. We start with the error condition: locationManager:didFailWithError. In the case of an error getting the current location, we already have a default message in place in the distanceLabel, so we just remove the waitView with the activity monitor and show the distanceView. If the user denied access to Core Location updates, we also shut down location manager updates. Implement locationManager:didFailWithError in ViewController.swift, as shown in Listing 21.5.

LISTING 21.5 Handling Location Manager Errors


 1:  func locationManager(manager: CLLocationManager,
 2:          didFailWithError error: NSError) {
 3:      if error.code == CLError.Denied.rawValue {
 4:          locMan.stopUpdatingLocation()
 5:      } else {
 6:          waitView.hidden = true
 7:          distanceView.hidden = false
 8:      }
 9:  }


In this error handler, we’re only worried about the case of the location manager not being able to provide us with any data. In line 3, we check the error code to make sure access wasn’t denied. If it was, the location manager is stopped (line 4).

In line 6, the wait view is hidden and the distance view, with the default text of Lots of miles to the Mothership, is shown (line 7).


Note

In this example, I use locMan to access the location manager. I could have used the manager variable provided to the method; there really wouldn’t have been a difference in the outcome. Because we have the variable property, however, using it consistently makes sense from the perspective of code readability.


Our next method (locationManager:didUpdateLocations) does the dirty work of calculating the distance to Cupertino. This brings us to one more hidden gem in CLLocation. We don’t need to write our own longitude/latitude distance calculations because we can compare two CLLocation instances with the distanceFromLocation method. In our implementation of locationManager:didUpdateLocations, we create a CLLocation instance for Cupertino and compare it to the instance we get from Core Location to get the distance in meters. We then convert the distance to miles, and if it’s more than 3 miles, we show the distance with an NSNumberFormatter used to add a comma if more than 1,000 miles. If the distance is less than 3 miles, we stop updating the location and congratulate the user on reaching “the Mothership.” Listing 21.6 provides the complete implementation of locationManager:didUpdateLocations.

LISTING 21.6 Calculating the Distance When the Location Updates


 1:  func locationManager(manager: CLLocationManager,
 2:          didUpdateLocations locations: [CLLocation]) {
 3:      let newLocation: CLLocation=locations[0]
 4:      if newLocation.horizontalAccuracy >= 0 {
 5:          let Cupertino:CLLocation = CLLocation(
 6:              latitude: kCupertinoLatitude,
 7:              longitude: kCupertinoLongitude)
 8:          let delta:CLLocationDistance =
 9:              Cupertino.distanceFromLocation(newLocation)
10:          let miles: Double = (delta * 0.000621371) + 0.5 // to miles
11:          if miles < 3 {
12:              // Stop updating the location
13:              locMan.stopUpdatingLocation()
14:              // Congratulate the user
15:              distanceLabel.text = "Enjoy the Mothership!"
16:          } else {
17:              let commaDelimited: NSNumberFormatter =
18:                  NSNumberFormatter()
19:              commaDelimited.numberStyle =
20:                  NSNumberFormatterStyle.DecimalStyle
21:              distanceLabel.text=commaDelimited
22:                  .stringFromNumber(miles)!+" miles to the Mothership"
23:          }
24:          waitView.hidden = true
25:          distanceView.hidden = false
26:      }
27:  }


The method starts off in line 3 by grabbing the first location returned in the locations array and storing it in newLocation.

Line 4 checks the new location to see whether it is useful information (an accuracy greater than zero). If it is, the rest of the method is executed; otherwise, we’re done.

Lines 5–7 create a CLLocation object (Cupertino) with the latitude and longitude of Cupertino.

Lines 8–9 create a CLLocationDistance variable named delta. Remember that CLLocationDistance isn’t an object; it is a double-precision floating-point number, which makes using it quite straightforward. The number is the distance between the CLLocation (Cupertino) object we just made and newLocation.

In line 10, the conversion of the distance from meters to miles is calculated and stored in miles.

Lines 11–16 check to see whether the distance calculated is less than 3 miles. If it is, the location manager is stopped, and the message Enjoy the Mothership is added to the distance label.

If the distance is greater than or equal to 3 miles, we initialize a number formatter object called commaDelimited in lines 17–18. Lines 19–20 set the style for the formatter.


Tip

Number formatters work by first setting a style for the object with the setNumberStyle method (in this case, NSNumberFormatterDecimalStyle). The NSNumberFormatterDecimalStyle setting defines decimal numbers with properly placed commas (for example, 1,500).

Once configured, the formatter can use the method stringFromNumber to output a nicely formatted number as a string.


Lines 21–22 set the distance label to show the number of miles (as a nicely formatted number).

Lines 24 and 25 hide the “wait” view and show the distance view, respectively.

Setting the Status Bar to White

One final method and we’re done with code. Like some of the other projects in this book, Cupertino has a dark background that obscures the iOS status bar. To lighten it up, add the method in Listing 21.7 to your ViewController.swift file.

LISTING 21.7 Setting the Status Bar Appearance in preferredStatusBarStyle


override func preferredStatusBarStyle() -> UIStatusBarStyle {
    return UIStatusBarStyle.LightContent
}


Updating the Project’s Plist file

Before the application can run, there’s one more piece of work we need to complete: adding the location prompt string to the application’s plist file. Using the Project Navigator, find and click the Info.plist file.

Expand the Information Property List entry, and then position your cursor over its name. A plus button appears to the right of the name. Click that button. Within the Key field, type NSLocationWhenInUseUsageDescription. Make sure the Type column is set to string, and then type a friendly message in the Value field, such as Let’s find Cupertino!. Figure 21.4 shows the setting within my version of the project.

Image

FIGURE 21.4 Add a message to display when the application prompts for location data.

Building the Application

Choose Run and take a look at the result. Your application should, after determining your location, display the distance to Cupertino, California, as shown in Figure 21.5.

Image

FIGURE 21.5 The Cupertino application in action showing the distance to Cupertino, California.


Tip

You can set simulated locations when your app is running. To do this, start the application, and then look at the bottom-center of the Xcode window. You will see the standard iOS “location” icon appear (among other controls). Click it to choose from a number of preset locations.

Another option is to use the Debug, Location from the menu bar in the iOS Simulator itself. There, you can easily configure a custom latitude and longitude for testing.

Note that you must set a location before responding to the app’s request to use your current location; otherwise, it assumes that no locations are available as soon as you click OK. If you make this mistake, stop the application’s execution in Xcode, uninstall the app from the iOS Simulator, and then run it again. This forces it to prompt for location information again.



Adding the iOS Blur

In a few places in the book, I’ve mentioned how the iOS blur effect can be added to your interface; this is another one of those places. In this hour’s projects, for example, you have several UIViews (waitView and distanceView) that are slightly transparent.

If you’ve followed along with my other blurry examples, adding the blur effect here should be no problem:

1. Remove the existing waitView and distanceView and their outlets. Add two Visual Effects Views with Blur to the design. These will become your new waitView and distanceView objects.

2. Add the appropriate labels and objects to the UIViews contained within effects views.

3. Add new outlets for the two effects views—naming them waitView and distanceView (the same as the old UIViews) respectively.

4. Because the effects views are named the same as the original UIViews, nothing else needs to change. The effects views (and their content) are hidden and shown appropriately.

That should do the trick. Anytime you want to use the blur effect, just replace your UIViews with visual effects views and you’ll have users squinting in no time.


Using the Magnetic Compass

The iPhone 3GS was the first iOS device to include a magnetic compass. Since its introduction, the compass has been added to the iPad. It is used in Apple’s Compass application and in the Maps application (to orient the map to the direction you are facing). The compass can also be accessed programmatically within iOS, which is what we look at now.

Implementation Overview

As an example of using the compass, we are going to enhance the Cupertino application and provide the users with a left, right, or straight-ahead arrow to get them pointed toward Cupertino. As with the distance indicator, this is a limited look at the potential applications for the digital compass. As you work through these steps, keep in mind that the compass provides information much more accurate than what we’re indicating with three arrows.

Setting Up the Project

Depending on your comfort level with the project steps we’ve already completed this hour, you can continue building this directly off the existing Cupertino application or create a copy. You’ll find a copy of Cupertino Compass, which includes the additional compass functionality for comparison, in this hour’s projects folder.

Open the Cupertino application project, and let’s begin by making some additions to support the use of the compass.

Adding the Direction Image Resources

The Images folder that you added to the asset catalog in the Cupertino project contains three arrow images: arrow_up.png, arrow_right.png, and arrow_left.png. If you removed these extra images from your first project (thought you were being clever, didn’t you?), add them back in now.

Planning the Variables and Outlets

To implement our new visual direction indicator, the view controller requires an outlet to an image view (UIImageView) to show the appropriate arrow. We’ll name this directionArrow.

We also need the last location that we were at, so we create another variable property called recentLocation. We need to store this because we’ll be doing a calculation on each heading update that uses the current location. We implement this calculation in a new method called headingToLocation:current.

Adding Radian/Degree Conversion Constants

Calculating a relative direction requires some rather complicated math. The good news is that someone has already written the formulas we need. To use them, however, we need to be able to convert between radians and degrees.

Add two constants to ViewController.swift, following the latitude and longitude for Cupertino. Multiplying by these constants allows us to easily perform our conversions:

let kDeg2Rad: Double = 0.0174532925
let kRad2Deg: Double = 57.2957795

Updating the User Interface

To update our application for the compass, we need to add a new image view to the interface, as follows:

1. Open the Main.storyboard file and the Object Library.

2. Drag an image view (UIImageView) onto the interface, positioning it above the waiting view.

3. Using the Attributes Inspector (Option-Command-4), set the image for the view to up_arrow.

4. We’ll be setting this dynamically in code, but choosing a default image helps with designing the view.

5. Use the Attributes Inspector to configure the image view as hidden; you can find this in the Drawing settings of the View section of the attributes. We don’t want to show a direction until we’ve calculated one.

6. Using the Size Inspector (Option-Command-5), set the width and height of the image view to be 150 points × 150 points.

7. Adjust the view so that it is centered nicely on the screen and not overlapping the “waiting” view. Feel free to shift things around as you see fit.

My final UI resembles Figure 21.6.

Image

FIGURE 21.6 The updated Cupertino application UI.

Creating and Connecting the Outlet

When finished with your interface, switch to the assistant editor and make sure that ViewController.swift is showing on the right. We need to make a single connection for the image view we just added. Control-drag from the image view to just below the last @IBOutlet line. When prompted, create a new outlet named directionArrow.

We can now wrap up our app by implementing heading updates. Switch back to the standard editor and open the ViewController.swift file.

Updating the Application Logic

To finish the project, we must do four things:

1. We need to ask our location manager instance to start updating us whenever it receives a change in heading.

2. We need to store the current location whenever we get an updated location from Core Location so that we can use the most recent location in the heading calculations.

3. We must implement logic to get a heading between our current location and Cupertino.

4. When we have a heading update, we need to compare it to the calculated heading toward Cupertino and change the arrow in the UI if any course adjustments need to be made.

Starting Heading Updates

Before asking for heading updates, we should check with the location manager to see whether heading updates are available via the class method headingAvailable. If heading updates aren’t available, the arrow images are never shown, and the Cupertino application works just as before. If headingAvailable returns true, set the heading filter to 10 degrees of precision and start the updates with startUpdatingHeading. Update the viewDidLoad method of the ViewController.swift file, as shown in Listing 21.8.

LISTING 21.8 Requesting Heading Updates


 1:  override func viewDidLoad() {
 2:      super.viewDidLoad()
 3:      // Do any additional setup after loading the view
 4:      locMan.delegate = self
 5:      locMan.desiredAccuracy = kCLLocationAccuracyThreeKilometers
 6:      locMan.distanceFilter = 1609; // a mile
 7:      locMan.requestWhenInUseAuthorization()
 8:      locMan.startUpdatingLocation()
 9:
10:      if CLLocationManager.headingAvailable() {
11:          locMan.headingFilter = 10 // 10 degrees
12:          locMan.startUpdatingHeading()
13:      }
14:  }


The squeaky-clean new code just takes up four lines. In line 10, we check to see whether a heading is available. If one is, we ask to be updated only if a change in heading is 10 degrees or more (line 11). In line 12, the location manager instance is asked to start updating us when there are heading changes. If you’re wondering why we didn’t just set a delegate, it’s because the location manager already has one set from our earlier code in line 4. This means that our class must handle both location updates and heading updates.

Storing the Recent Location

To store the recent location, we need to declare a new variable property that we can use in our methods; this, like the location manager, should be declared after the class line in ViewController.swift. Unlike the location manager, however, the recent location needs to be a variable property—not a constant—because it is going to be updated over time.

Locations are managed as objects of type CLLocation; we’ll name ours recentLocation. Update the code at the top of ViewController.swift to include this new variable. The block should now read as follows:

import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

    @IBOutlet weak var waitView: UIView!
    @IBOutlet weak var distanceView: UIView!
    @IBOutlet weak var distanceLabel: UILabel!
    @IBOutlet weak var directionArrow: UIImageView!

    let locMan: CLLocationManager = CLLocationManager()
    var recentLocation: CLLocation!

Next, we need to add a line to set recentLocation to the value of newLocation in the locationManager:didUpdateLocations method. We should also stop updating the heading if we are within 3 miles of the destination, just as we stopped updating the location. Listing 21.9 shows these two changes to the method.

LISTING 21.9 Storing the Recently Received Location for Later Use


 1:  func locationManager(manager: CLLocationManager,
 2:          didUpdateLocations locations: [CLLocation]) {
 3:      let newLocation: CLLocation=locations[0]
 4:      if newLocation.horizontalAccuracy >= 0 {
 5:          recentLocation=newLocation
 6:          let cupertino:CLLocation = CLLocation(
 7:              latitude: kCupertinoLatitude,
 8:              longitude: kCupertinoLongitude)
 9:          let delta:CLLocationDistance =
10:              cupertino.distanceFromLocation(newLocation)
11:          let miles: Double = (delta * 0.000621371) + 0.5 // to miles
12:          if miles < 3 {
13:              // Stop updating the location and heading
14:              locMan.stopUpdatingLocation()
15:              locMan.stopUpdatingHeading()
16:              // Congratulate the user
17:              distanceLabel.text = "Enjoy the Mothership!"
18:          } else {
19:              let commaDelimited: NSNumberFormatter =
20:                  NSNumberFormatter()
21:              commaDelimited.numberStyle =
22:                  NSNumberFormatterStyle.DecimalStyle
23:              distanceLabel.text=commaDelimited
24:                  .stringFromNumber(miles)!+" miles to the Mothership"
25:          }
26:          waitView.hidden = true
27:          distanceView.hidden = false
28:      }
29:  }


The only changes from the previous tutorial are the addition of line 5, which stores the incoming location in recentLocation, and line 15, which stops heading updates if we are sitting in Cupertino.

Calculating the Heading to Cupertino

In the previous two sections, we avoided doing calculations with latitude and longitude. This time, it requires just a bit of computation on our part to get a heading to Cupertino and then to decide whether that heading is straight ahead or requires the user to spin to the right or to the left.

Given two locations such as the user’s current location and the location of Cupertino, it is possible to use some basic geometry of the sphere to calculate the initial heading the user would need to use to reach Cupertino. A search of the Internet quickly finds the formula in JavaScript (copied here in the comment), and from that we can easily implement the algorithm in Objective-C and provide the heading. We add this as a new method, headingToLocation:current, that takes two locations and returns a heading that can be used to reach the destination from the current location.

Add the headingToLocation:current method to the ViewController.swift file, as in Listing 21.10.

LISTING 21.10 Calculating a Heading to a Destination


 /*
 * According to Movable Type Scripts
 * http://mathforum.org/library/drmath/view/55417.html
 *
 *  Javascript:
 *
 * var y = Math.sin(dLon) * Math.cos(lat2);
 * var x = Math.cos(lat1)*Math.sin(lat2) -
 * Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
 * var brng = Math.atan2(y, x).toDeg();
 */
 1:  func headingToLocation(desired: CLLocationCoordinate2D,
 2:      current: CLLocationCoordinate2D) -> Double {
 3:      // Gather the variables needed by the heading algorithm
 4:      let lat1:Double = current.latitude*kDeg2Rad
 5:      let lat2: Double = desired.latitude*kDeg2Rad
 6:      let lon1: Double  = current.longitude
 7:      let lon2: Double = desired.longitude
 8:      let dlon: Double = (lon2-lon1)*kDeg2Rad
 9:
10:      let y: Double = sin(dlon)*cos(lat2)
11:      let x: Double =
12:          cos(lat1)*sin(lat2) - sin(lat1)*cos(lat2)*cos(dlon)
13:
14:      var heading:Double = atan2(y,x)
15:      heading=heading*kRad2Deg
16:      heading=heading+360.0
17:      heading=fmod(heading,360.0)
18:      return heading
19:  }


Don’t worry about the math here. I didn’t make it up, and there’s no reason you need to understand it. What you do need to know is that, given two locations—one current and one desired (the destination)—this method returns a floating-point number in degrees. If the returned value is 0, we need to head north to get where we’re going. If it’s 180, we need to go south (and so on).

If you’re interested in the history of the process and how it works, look up “great circle navigation” on your search engine of choice.

Handling Heading Updates

The last piece of our implementation is handling heading updates. The ViewController class implements the CLLocationManagerDelegate protocol, and as you learned earlier, one of the optional methods of this protocol, locationManager:didUpdateHeading, provides heading updates anytime the heading changes by more degrees than the headingFilter amount.

For each heading update our delegate receives, we should use the user’s current location to calculate the heading to Cupertino, compare the desired heading to the user’s current heading, and then display the correct arrow image: left, right, or straight ahead.

For these heading calculations to be meaningful, we need to have the current location and some confidence in the accuracy of the reading of the user’s current heading. We check these two conditions in an if statement before performing the heading calculations. If this sanity check does not pass, we hide the directionArrow.

Because this heading feature is more of a novelty than a true source of directions (unless you happen to be a bird or in an airplane), there is no need to be overly precise. Using +/–10 degrees from the true heading to Cupertino as close enough to display the straight-ahead arrow. If the difference is greater than 10 degrees, we display the left or right arrow based on whichever way would result in a shorter turn to get to the desired heading. Implement the locationManager:didUpdateHeading method in the ViewController.swift file, as shown in Listing 21.11.

LISTING 21.11 Handling the Heading Updates


 1:  func locationManager(manager: CLLocationManager,
 2:      didUpdateHeading newHeading: CLHeading) {
 3:      if (recentLocation != nil && newHeading.headingAccuracy >= 0) {
 4:          let cupertino:CLLocation = CLLocation(
 5:              latitude: kCupertinoLatitude,
 6:              longitude: kCupertinoLongitude)
 7:          let course: Double = headingToLocation(
 8:              cupertino.coordinate,
 9:              current:recentLocation.coordinate)
10:          let delta: Double = newHeading.trueHeading - course
11:          if (abs(delta) <= 10) {
12:              directionArrow.image = UIImage(named: "up_arrow.png")
13:          } else {
14:              if (delta > 180) {
15:                directionArrow.image = UIImage(named: "right_arrow.png")
16:              }
17:              else if (delta > 0) {
18:                directionArrow.image = UIImage(named: "left_arrow.png")
19:              }
20:              else if (delta > -180) {
21:                directionArrow.image = UIImage(named: "right_arrow.png")
22:              }
23:              else {
24:                directionArrow.image = UIImage(named: "left_arrow.png")
25:              }
26:          }
27:          directionArrow.hidden = false
28:      } else {
29:          directionArrow.hidden = true
30:      }
31:  }


We begin in line 3 by checking to see whether we have valid information stored for recentLocation and a meaningful heading accuracy. If these conditions aren’t true, the method hides the directionArrow image view in line 29.

Lines 4–6 create a new CLLocation object that contains the location for Cupertino. We use this for getting a heading from our current location (stored in recentLocation) in lines 7–9. The heading that would get us to our destination is stored as a floating-point value in course.

Line 10 is a simple subtraction, but it is the magic of the entire method. Here we subtract the course heading we calculated from the one we’ve received from core location (newHeading.trueHeading). This is stored as a floating-point number in the variable delta.

Let’s think this through for a second. If the course we should be going in is north (heading 0) and the heading we’re actually going in is also north (heading 0), the delta is 0, meaning that we do not need to make a course correction. However, if the course we want to take is east (a heading of 90), and the direction we are going in is north (a heading of 0), the delta value is –90. Need to be headed west but are traveling east? The delta is –270, and we should turn toward the left. By looking at the different conditions, we can come up with ranges of delta values that apply to the different directions. This is exactly what happens in lines 14–25. You can try the math yourself if you need convincing. Line 11 differs a bit; it checks the absolute value of delta to see whether we’re off by more than 10 degrees. If we aren’t, the arrow keeps pointing forward.


Note

We don’t have a backward-pointing arrow here, so any course correction needs to be made by turning left or right. Understanding this can be helpful in seeing why we compare the delta value to greater than 180 and greater than –180 rather than greater than or equal to. 180/–180 is exactly in the opposite direction we’re going, so left or right is ambiguous. Up until we reach 180/–180, however, we can provide a turn direction. At exactly 180, the else clause in line 23 kicks in and we turn left. Just because.


Building the Application

Run the project. If you have a device equipped with an electromagnetic compass, you can now spin around in your office chair and see the arrow images change to show you the heading to Cupertino (see Figure 21.7). If you run the updated Cupertino application in the iOS Simulator, you might not see the arrow; heading updates seem to be hit or miss in the Simulator.

Image

FIGURE 21.7 The completed Cupertino application with compass.

Usually miss.

Further Exploration

In the span of an hour, you covered a great deal of what Core Location has to offer. I recommend that you spend time reviewing the Core Location Framework Reference as well as the Location Awareness Programming Guide, both of which are accessible through the Xcode documentation.

In addition, I greatly recommend reviewing Movable Type Scripts documentation on latitude and longitude functions (http://www.movable-type.co.uk/scripts/latlong.html). Although Core Location provides a great deal of functionality, there are some things (such as calculate a heading/bearing) that it just currently cannot do. The Movable Type Scripts library should give you the base equations for many common location-related activities.


Apple Tutorials

LocateMe (accessible through the Xcode documentation interface): A simple Xcode project to demonstrate the primary functions of Core Location.


Summary

In this hour, you worked with the powerful Core Location toolkit. As you saw in the application example, this framework can provide detailed information from an iDevice’s GPS and magnetic compass systems. Many modern applications use this information to provide data about the world around the user or to store information about where the user was physically located when an event took place.

You can combine these techniques with the Map Kit from the previous hour to create detailed mapping and touring applications.

Q&A

Q. Should I start receiving heading and location updates as soon as my application launches?

A. You can, as we did in the tutorial, but be mindful that the hardware’s GPS features consume quite a bit of battery power. After you establish your location, turn off the location/heading updates.

Q. Why do I need that ugly equation to calculate a heading? It seems overly complicated.

A. If you imagine two locations as two points on a flat grid, the math is easier. Unfortunately, the Earth is not flat but a sphere. Because of this difference, you must calculate distances and headings using the great circle (that is, the shortest distance between two points on a curved surface).

Q. Can I use Core Location and Map Kit to provide turn-by-turn directions in my application?

A. Yes and no. You can use Core Location and Map Kit as part of a solution for turn-by-turn directions, and some developers do this, but they are not sufficiently functional on their own. In short, you have to license some additional data to provide this type of capability.

Workshop

Quiz

1. To work with location data, you must create an instance of which of the following?

a. CLLocationManager

b. CILocationManager

c. CLManager

d. CoreData

2. The framework responsible for providing location services in iOS is known as what?

a. Core Data

b. Location manager

c. Core Location

d. Location data manager

3. A traditional mechanical compass points to where?

a. True north

b. True south

c. Magnetic south

d. Magnetic north

4. Before using the iOS location data in an application, what must you request?

a. Memory

b. Authorization

c. Network resources

d. Data storage

5. iOS represents a location using which class?

a. CLLocationData

b. CLPlace

c. CLLocation

d. CLLocationManager

6. To begin receiving location updates, you must use what location manager method?

a. getLocation

b. beginUpdatingLocation

c. startUpdatingLocation

d. getLocationUpdates

7. A class that should receive location updates will need to implement which protocol?

a. CLLocationManagerDelegate

b. CLLocationManagerUtility

c. LocationDelegate

d. CLLocationDelegate

8. To prevent an overwhelming number of heading updates, you can set a what?

a. Heading stop

b. Heading filter

c. Heading slowdown

d. Heading limit

9. To request that an application always have access to the location, you should use what method?

a. requestConstantAuthorization

b. requestPermanentAuthorization

c. requestPerpetualAuthorization

d. requestAlwaysAuthorization

10. The accuracy of location updates is managed using which location manager variable property?

a. requiredAccuracy

b. desiredAccuracy

c. reportAccuracy

d. setAccuracy

Answers

1. A. An instance of the Core Location location manager (CLLocationManager) class is needed to begin working with locations.

2. C. The Core Location framework provides location services for iOS applications.

3. D. A traditional compass points to magnetic north, versus geographic north.

4. B. You must request authorization from the user before attempting to use location services.

5. C. Instances of the CLLocation class represent geographic locations within iOS.

6. C. Use the startUpdatingLocation method to begin receiving location updates.

7. A. A class must implement the CLLocationManagerDelegate protocol in order to handle location information.

8. B. Implementing a heading filter can keep your application from being overwhelmed by insignificant changes to a device’s orientation.

9. D. Although not recommended by Apple, the requestAlwaysAuthorization method can be used to give an application access to location information regardless of its state.

10. B. The desiredAccuracy variable property can be set to a variety of constants that generate location events that will be accurate for walking, driving, and so on.

Activities

1. Adopt the Cupertino application to be a guide for your favorite spot in the world. Add a map to the view that displays your current location.

2. Identify opportunities to use the location features of core location. How can you enhance games, utilities, or other applications with location-aware features?

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

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