Where Are You - Learn iOS 8 App Development, Second Edition (2014)

Learn iOS 8 App Development, Second Edition (2014)

Chapter 17. Where Are You?

If you think the accelerometer, gyroscope, and magnetometer are cool, you’re going to love this chapter. In addition to those instruments, many iOS devices contain radio receivers allowing them to triangulate their position by timing radio signals they receive from a network of satellites—either the Global Positioning System or the Russian Global Navigation Satellite System. This technology is generically referred to as GPS.

What does that mean to you? As a user, it means your iOS device knows where it is on the planet. As a developer, it means your app can get information about the device’s location and use that to show your user where they are, what’s around them, where they’ve come from, or how to get to where they want to go. In this chapter, you will do the following:

· Collect location information

· Display a map showing the user’s current location

· Add custom annotations to a map

· Monitor the user’s movement and offer direction

· Create an interface for changing map options

This chapter will use two iOS technologies: Core Location and Map Kit. Core Location provides the interface to the GPS satellite receivers and provides your app with data about where the device is located, in a variety of forms. Map Kit supplies the view objects and tools to display, annotate, and animate maps. The two can be used separately or together.

Creating Pigeon

The app for this chapter is called Pigeon. It’s a utility that lets you remember your current location on a map. Later it will show you where you are and where the marked location is so you can fly back to it. Figure 17-1 shows the design for Pigeon.

image

Figure 17-1. Pigeon design

The app has a map and three buttons. The middle button remembers your current location and drops a pin into the map to mark it. When you move away from that location, the map displays where you are, the saved location, and a line showing the direction back. A trash button forgets the saved location, and an info button lets the user change map display options. Let’s get started.

Start by creating the project and laying out the interface. In Xcode, create a new project as follows:

1. Use the Single View Application template.

2. Name the project Pigeon.

3. Use the Swift language.

4. Set devices to Universal.

Select the Main.storyboard file. Add a toolbar to the bottom of the interface. Add and configure toolbar button items as follows (from left to right):

1. Add a Bar Button Item and set its identifier to Trash.

2. Add a Flexible Space Bar Button Item.

3. Add a Bar Button Item and set its title to Remember Location.

4. Add a Flexible Space Bar Button Item.

5. Add a Button (not a Bar Button Item) and set its type to Info Light.

From the object library, add a map view object to fill the rest of the interface. Set the following attributes for the Map View object:

1. Check Shows User Location.

2. Check Allows Zooming.

3. Uncheck Allows Scrolling.

4. Uncheck 3D Perspective.

Complete the layout by choosing the Add Missing Constraints to View Controller command, either by selecting the Editor image Resolve Auto Layout Issues submenu or by clicking the resolve auto layout issues button at the bottom of the editor pane. The finished interface should look likeFigure 17-2.

image

Figure 17-2. Pigeon interface

You’ll need to wire up these views to your controller, so do that next. Switch to the assistant editor and make sure ViewController.swift appears in the pane on the right. You’ll be using the Map Kit framework, so add an import statement to pull in the Map Kit declarations (new code in bold).

import UIKit
import MapKit

Add the outlet for the map view and stub functions for two actions to the ViewController class.

@IBOutlet var mapView: MKMapView!

@IBAction func dropPin(sender: AnyObject!) {
}

@IBAction func clearPin(sender: AnyObject!) {
}

Connect the mapView outlet to the map view object, as shown in Figure 17-3. Connect the actions of the left and center toolbar buttons to the clearPin(_:) and dropPin(_:) functions, respectively. Now you’re ready to begin coding the actions.

image

Figure 17-3. Connecting the map outlets

Collecting Location Data

Getting location data follows the same pattern you used to get gyroscope and magnetometer data in Chapter 16, with only minor modifications. The basic steps are as follows:

1. If precise (GPS) location information is a requirement for your app, add the gps value to the app’s Required Device Capabilities property.

2. Create an instance of CLLocationManager.

3. Declare the reason your app needs location information.

4. Request permission to gather location information.

5. Check to see whether location services are available using the locationServicesAvailable() or authorizationStatus() function.

6. Adopt the CLLocationManagerDelegate protocol in one of your classes and make that object the delegate for the CLLocationManager object.

7. Call startUpdatingLocation() to begin collecting location data.

8. The delegate object will receive function calls whenever the device’s location changes.

9. Send stopUpdatingLocation() when your app no longer needs location data.

The significant difference between using CLLocationManager and CMMotionManager is that you can create multiple CLLocationManager objects and data is delivered to its delegate object (rather than requiring your app to pull the data or push it to an operation queue).

Another difference is that location data may not be available, even on devices that have GPS hardware. There are a lot of reasons this might be true. The user may have location services turned off. They may be somewhere they can’t receive satellite signals. The device may be in “airplane mode,” which doesn’t permit the GPS receivers to be energized. Or your app may specifically have been denied access to location information. It doesn’t really matter why. You need to check for the availability of location data and deal with the possibility that you can’t get it.

Finally, there are a number of functions for getting location data depending on the precision of the data and how quickly it’s delivered. Knowing that the user moved 20 feet to the left is a different problem from knowing that they’ve arrived at work. I’ll describe the different kinds of location monitoring toward the end of the chapter.

Pigeon needs precise location information that only GPS hardware can deliver. Select the Pigeon project in the project navigator, select the Pigeon target (from the upper-left pop-up menu, as shown in Figure 17-4, or from the target list), switch to the Info tab, and locate the Required device capabilities option in the Custom iOS Target Properties group. Click the + button and add a gps requirement, as shown in Figure 17-4.

image

Figure 17-4. Adding the gps device requirement

Asking for Permission

The next two steps are to declare the reason why your app needs to collect location information and ask for permission to get it. Starting in iOS 8, your app must explicitly explain to the user why it’s gathering location information and the scope of its collection process.

Your statement of intent is a property of your app. Since you’re still in the target’s Info section, add that now. Hover over any top-level property, click the + button to create a new property, name the property NSLocationWhenInUseUsageDescription, and type Pigeon wants to know where you are for its value, as shown in Figure 17-5.

image

Figure 17-5. Adding a location usage description

The next step is to ask permission. Do this in the AppDelegate class (in the AppDelegate.swift file). Edit the beginning of the class so it looks like this (new code in bold):

import UIKit
import CoreLocation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let locationManager = CLLocationManager()

func application(application: UIApplication!, image
didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
locationManager.requestWhenInUseAuthorization()
return true
}

You added this code to the AppDelegate class because this step is something you’re doing for the whole app, not a specific view controller. It’s appropriate that it be placed in your app’s delegate object. Also, the application(_:,didFinishLaunchWithOptions:) function is one of the earliest opportunities for your code to execute. It’s called immediately after the core app initialization is finished but before your app’s first view controller appears or the app starts running. It’s the perfect place to do things that need to be done early and once.

The first time your app runs, the requestWhenInUseAuthorization() function presents a dialog to the user asking them for access to their location information, as shown in Figure 17-6. Your reason for wanting this information (inNSLocationWhenInUseUsageDescription) is included in that alert.

image

Figure 17-6. Location authorization alert

There are two request authorization functions. You call either requestWhenInUseAuthorization() or requestAlwaysAuthorization(). The former requests permission to gather location information while your app is active. The latter requests permission to gather location information all the time. Since Pigeon uses location information only while it’s running, you called the first function.

It’s possible for your app to monitor and record location changes when it’s not the active app and even when it’s not running. I’ll discuss alternate location monitoring services later in this chapter.

Note Technically, the requestWhenInUseAuthorization() and requestAlwaysAuthorization() functions prompt the user only if your app’s current authorization status is .NotDetermined. This is true when your app is first installed. If its status is anything else (.Restricted, .Denied, .Authorized, or .AuthorizedWhenInUse), these calls do nothing because your app’s authorization (or lack thereof) has already been determined.

Starting Location Collection

You’re now probably thinking that I’m going to have you add some code to do the following:

· Make the ViewController adopt the CLLocationManagerDelegate protocol

· Implement the locationManager(_:,didUpdateLocations:) delegate function to process the location updates

· Make the ViewController the location manager’s delegate

· Call startUpdatingLocation() to begin collecting location data

But you’re not going to do any of that.

Now I’m sure you wondering why not, so let me explain. Pigeon uses both location services and the Map Kit. The Map Kit includes the MKMapView object, which displays maps. Among its many talents, it has the ability to monitor the device’s current location and display that on the map. It will even notify its delegate when the user’s location changes.

For this particular app, MKMapView is already doing all of the work for you. When you ask it to display the user’s location, it creates its own instance of CLLocationManager and begins monitoring location changes and updating the map and its delegate. The end result is thatMKMapView has all of the information that Pigeon needs to work.

Note Pigeon is a little bit of an anomaly; you’ll configure the map view so it always tracks the user’s location and the map view is always active. If this wasn’t the case, then relying on the map view to locate the user wouldn’t be a solution and you’d have to resort to usingCLLocationManager in the usual way.

This is a good thing. All of that CLLocationManager code would look so much like the code you wrote in Chapter 16 that it would make this app a little boring, and I certainly don’t want you to get bored. Or maybe you haven’t read Chapter 16 yet, in which case you have something to look forward to.

Regardless, all you need to do is set up MKMapView correctly. Let’s do that now.

Using a Map View

Your map view object has already been added to the interface and connected to the mapView outlet. You’ve also used the attributes inspector to configure the map view so it shows (and tracks) the user’s location, and you disallowed user scrolling. There’s one more setting that you need to make, and you can’t set it from the attributes inspector.

Select the ViewController.swift file and locate the viewDidLoad() function. At the end, add this statement:

mapView.userTrackingMode = .Follow

This sets the map’s tracking mode to “follow the user.” There are three tracking modes—which I’m sure you’ve seen in places like Apple’s Maps app—listed in Table 17-1.

Table 17-1. User Tracking Modes

MKUserTrackingMode

Description

.None

The map does not follow the user’s location.

.Follow

The map is centered at the user’s current location, and it moves when the user moves.

.FollowWithHeading

The map tracks the user’s current location, and the orientation of the map is rotated to indicate the user’s direction of travel.

The code you added to viewDidLoad() sets the tracking mode to follow the user. The combination of the showsUserLocation property and the tracking mode force the map view to begin gathering location data, which is what you want.

If you’ve played with the Maps app, you also know that you can “break” the tracking mode by manually panning the map. You disabled panning for the map view, but there are still circumstances where the tracking mode will revert to MKUserTrackingMode.None. To address that, you need to add code to catch the situation where the tracking mode changes and “correct” it, if necessary.

That information is provided to the map view’s delegate. Wouldn’t it be great if your ViewController object were the delegate for the map view? I thought so too.

Adopt the MKMapViewDelegate protocol in ViewController (new code in bold):

class ViewController: UIViewController, MKMapViewDelegate {

Now add this map view delegate function:

func mapView(mapView: MKMapView!, didChangeUserTrackingMode mode:image
MKUserTrackingMode, animated: Bool) {
if mode == .None {
mapView.userTrackingMode = .Follow
}
}

This function is called whenever the tracking mode for the map changes. It simply sees whether the mode has changed to “none” and resets it to tracking the user.

Of course, this function is called only if your ViewController object is the delegate object for the map view. Select the Main.storyboard file. Select the map view object and use the connections inspector to connect the map view’s delegate outlet to the view controller, as shown in Figure 17-7.

image

Figure 17-7. Connecting the map view’s delegate outlet

You’ve done everything you need to see the map view in action, so fire it up. Run your app in the simulator or on a provisioned device. You should see something like what’s shown in Figure 17-8.

image

Figure 17-8. Testing map view

The first time your app runs, iOS will ask the user if it’s OK for your app to collect location data. Tap Allow, or this is going to be a really short test. Once it’s granted permission, the map locates your device and centers the map at your location.

The iOS simulator will emulate location data, allowing you to test location-aware apps. In the Debug menu you’ll find a number of choices in the Location submenu (the second image in Figure 17-8). Choose the Custom Location item to enter the longitude and latitude of your simulated location. There are also a few preprogrammed locations, such as the Apple item, also shown in Figure 17-8.

Some of the items play back a recorded trip. Currently the choices are City Bicycle Ride, City Run, and Freeway Drive. Selecting one starts a series of location changes, which the map will track, as though the device was on a bicycle, accompanying a runner, or in a car. Go ahead and try one; you know you want to.

The map can also be zoomed in and out by pinching or double-tapping. You can’t scroll the map because you disabled that option in Interface Builder.

While the freeway drive is playing back, add the code to mark your location on the map.

Decorating Your Map

There are three ways of adding visual elements to a map: annotations, overlays, and subviews.

An annotation identifies a single point on the map. It can appear anyway you like, but iOS provides classes that mark the location with a recognizable “map pin” image. Annotations can optionally display a callout that consists of a title, subtitle, and accessory views. The callout appears above the pin when selected (tapped).

An overlay identifies a path or region on the map. Overlays can draw lines (like driving directions), highlight arbitrary areas (like a city park), and note points of interest (like a landmark). Like with annotations, you can draw anything you want on the map, but iOS provides classes that draw simple overlays for you.

A subview is like any other subview. MKMapView is a subclass of UIView, and you are free to add custom UIView objects to it. Use subviews to add additional controls or indicators to the map.

Annotations and overlays are attached to the map. They are described using map coordinates (which I’ll talk about later), and they move when the map moves. Subviews are positioned in the local graphics coordinate system of the MKMapView object. They do not move with the map.

Your Pigeon app will create an annotation—that is, “put a pin”—at the user’s current location when they tap the “remember location” button. The trash button will discard the pin. You already wrote stubs for the dropPin(_:) and clearPin(_:) action functions. It’s time to flesh them out.

Adding an Annotation

When the user taps the “remember location” button, you’ll capture their current location and add an annotation to the map. I thought it would be nice if the user could choose a label for the location to make it easier to remember what they’re trying to remember. To accomplish all that, you’ll use a UIAlertController. In ViewController.swift , start by finishing the dropPin(_:) function.

@IBAction func dropPin(sender: AnyObject!) {
let alert = UIAlertController(title: "What's Here?",
message: "Type a label for this location.",
preferredStyle: .Alert)
alert.addTextFieldWithConfigurationHandler(nil)
let cancelAction = UIAlertAction(title: "Cancel",
style: .Cancel,
handler: nil)
alert.addAction(cancelAction)
let okAction = UIAlertAction(title: "Remember",
style: .Default,
handler: { (_) in
if let textField = alert.textFields?[0] as? UITextField {
var label = "Over Here!"
if let text = textField.text {
let trimmed = text.stringByTrimmingCharactersInSet(image
NSCharacterSet.whitespaceAndNewlineCharacterSet())
if (trimmed as NSString).length != 0 {
label = trimmed
}
}
self.saveAnnotation(label: label)
}
})
alert.addAction(okAction)
presentViewController(alert, animated: true, completion: nil)
}

This function begins by presenting an alert view, configured so the user can enter some text. The code then adds two alert actions (buttons), one to cancel and the other to remember the location. It’s the second one that’s interesting. If the user taps the remember action, its handler gets the text the user typed in (if any), cleans it up, and supplies a default label if it’s empty. The new label is then passed to the saveAnnotation(label:) function.

The other action function is pretty simple.

@IBAction func clearPin(sender: AnyObject!) {
clearAnnotation()
}

The work of creating and clearing the annotation falls on the saveAnnotation(label:) and clearAnnoation() functions. Start with the saveAnnotation(label:) function.

var savedAnnotation: MKPointAnnotation?

func saveAnnotation(# label: String) {
if let location = mapView.userLocation?.location {
clearAnnotation()
let annotation = MKPointAnnotation()
annotation.title = label
annotation.coordinate = location.coordinate
mapView.addAnnotation(annotation)
mapView.selectAnnotation(annotation, animated: true)
savedAnnotation = annotation
}
}

The first step is to get the user’s current location. Remember that the map view has been tracking their location since the app was started, so it should have a pretty good idea of where they are by now. You must, however, consider the possibility that the map view doesn’t know, in which caseuserLocation won’t contain a value. The user may have disabled location services, is running in “airplane mode,” or is spelunking. Regardless, if there’s no location, there’s nothing to do.

If the map view does know the user’s location, it extracts their map coordinates (longitude and latitude) and uses that to create an MKPointAnnotation object. This is a simple annotation the marks a location on the map. The annotation is assigned a title and its location, added to the map, and then selected. Selecting the annotation is the same as tapping it, causing the label to appear in its callout.

The app is almost finished; you just need the clearAnnotation() function.

func clearAnnotation() {
if let annotation = savedAnnotation {
mapView.removeAnnotation(annotation)
savedAnnotation = nil
}
}

That was simple.

Run the app and give it a try. Tap the “remember location” button and enter a label, and a pin appears at your current location, as shown in Figure 17-9.

image

Figure 17-9. Testing the annotation

Map Coordinates

The coordinates of the annotation object were set to the coordinates of the user’s location (provided by the map view). But what are these “coordinates?” Map Kit uses three coordinate systems, listed in Table 17-2.

Table 17-2. Map Coordinate Systems

Coordinate System

Description

Latitude and Longitude

The latitude and longitude, and sometimes altitude, of a position on the planet. These are called map coordinates.

Mercator

The position (x,y) on the Mercator map of the planet. A Mercator map is a cylindrical projection of the planet’s surface onto a flat map. The Mercator map is what you see in the map view. Positions on the Mercator map are called map points.

Graphics

The graphics coordinates in the interface, used by UIView. These are referred to simply as points.

Map coordinates (longitude and latitude) are the principal values used to identify locations on the map, stored in a CLLocationCoordinate2D structure. They are not xy coordinates, so calculating distance and heading between two coordinates is a nontrivial exercise that’s best left to location services and Map Kit. Annotations and overlays are positioned at map coordinates.

Map points are xy positions in the Mercator map projection. Being xy coordinates on a flat plane, calculating angles and distances is much simpler. Map points are used when drawing overlays. This simplifies drawing and reduces the math involved.

Note The Mercator projection is particularly convenient for navigation because a straight line between any two points on a Mercator map describes a heading the user can follow to get from one to the other. The disadvantage is that east-west distances and north-south distances are not to the same scale—except at the equator.

Map points are eventually translated into graphic coordinates, so they can appear somewhere on the screen. There are functions to translate map coordinates into graphic coordinates. Additional functions translate the other way.

Adding a Little Bounce

Your map pin appears on the map, and it moves around with the map. You can tap it to show, or hide, its callout. This is pretty impressive, considering you needed only a few lines of code to create it. We do, however, love animation, and I’m sure you’ve seen map pins that “drop” into place. Your pin doesn’t drop; it just appears. So, how do you get your map pin to animate, change its color, or customize it in any other way? The answer is to use a custom annotation view.

An annotation in a map is actually a pair of objects: an annotation object and an annotation view object. An annotation object associates information with a coordinate on the map—the data model. An annotation view object is responsible for how that annotation looks—the view. If you want to customize how an annotation appears, you must supply your own annotation view object.

You do this by implementing the mapView(_:,viewForAnnotation:) delegate function. When the map view wants to display an annotation, it calls this function of its delegate passing it the annotation object. The function’s job is to return an annotation view object that represents that annotation. If you don’t implement this function or return nil for an annotation, the map view uses its default annotation view, which is the plain map pin you’ve already seen.

Add this function to ViewController.swift:

func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!)image
-> MKAnnotationView! {
if annotation === mapView.userLocation {
return nil
}

let pinID = "Save"
var pinView: MKPinAnnotationView!
pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(pinID)image
as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation,image
reuseIdentifier: pinID)
pinView.canShowCallout = true
pinView.animatesDrop = true
}
return pinView
}

The first statement compares the annotation to the map view’s user annotation object. The user annotation object, like any other annotation object, represents the user’s position in the map. The map view automatically added it when you asked it to display the user’s location. This automatic annotation is available via the map view’s userLocation property but also appears in the general collection of annotations. If you return nil for this annotation, the map view uses its default user annotation view—the pulsing blue dot we’re all familiar with. If you want to represent the user’s location some other way, this is where you’d provide that view.

The rest of the code works just like the table view cell code from Chapter 4. The map view maintains a cache of reusable MKAnnotationView objects that you recycle using an identifier. Your map uses only one kind of annotation view: a standard map pin view provided by theMKPinAnnotationView class. The pin is configured to display callouts and animate itself (“drop in”) when added to the map.

Tip If you want to give the user the ability to move the pin they just dropped, all you have to do is set the draggable property of the annotation view object to true.

Run the app again. Now when you save the location, the pin animates its insertion into the map, which is a lot more appealing.

Your mapView(_:,viewForAnnotation:) delegate function can return a customized version of a built-in annotation view class, as you’ve done here. MKPinAnnotationView can display pins of different colors, can allow or disallow callouts, can have custom accessory views in its callout, and so on. Alternatively, you can subclass MKAnnotationView and create your own annotation view, with whatever custom graphics and animations you want. You could represent the user’s location as a waddling duck. Let your imagination run wild.

Showing the Way Home

The second technique for decorating maps employs overlays. Overlays occupy an area of the map, not just a single point. They’re intended to represent things such as driving instructions, geographic features, and so on.

Overlays are similar to annotations. There’s an overlay object that describes where the overlay is on the map—the data model. And there’s a companion overlay renderer object that’s responsible for drawing that overlay—the view.

Note Both MKAnnotation and MKOverlay are protocols, not classes. Any class can provide annotation or overlay information for a map view by adopting the appropriate protocol and implementing the required functions. Protocols are explained in Chapter 20.

Unlike annotations, the overlay renderer isn’t a UIView object. It’s a lightweight object that simply contains the code to paint the overlay directly into the map view’s graphics context. In brief, it has a draw...() function, just like UIView’s drawRect(_:) function, but nothing else. This means you can’t animate an overlay or use any of the standard UIView objects.

Also like annotations, iOS provides a set of useful overlay and overlay renderer classes. Let’s use the MKPolyline and MKPolylineRenderer classes to draw a line between the user’s saved location and their current location.

Adding Overlays

In Pigeon, you’ll dynamically add an overlay whenever the user’s location changes. “When does that happen?” you ask. It happens whenever the user moves. “How do I find out about it?” you ask. The map view notifies your delegate whenever the user’s location is being displayed in the view and their location has changed. Add that delegate function to ViewController now—you can find the completed app in the Learn iOS Development Projects image Ch 17 image Pigeon-2 folder.

func mapView(mapView: MKMapView!, didUpdateUserLocation userLocation:
MKUserLocation!) {
clearOverlay()
if let saved = savedAnnotation {
if let user = userLocation {
var coords = [ user.coordinate, saved.coordinate ]
returnOverlay = MKPolyline(coordinates: &coords, count: 2)
mapView.addOverlay(returnOverlay)
}
}
}

Whenever the user’s location changes, start by discarding any existing overlay—it’s no longer accurate. Get both the saved location and the user’s current location. If both are known, create an MKPolyline overlay data model with two points—the map coordinates of the user and the saved location—and add it to the map. That’s it.

Note MKPolyline can describe an arbitrary complex line, much like a Bézier path. It’s intended to describe things such as travel routes, geopolitical boundaries, and so on. There’s also an MKPolygon class for solid shapes (like shading the area occupied by a national park) and a special MKGeodesicPolyline that’s useful for representing very long distances, like the flight path of an aircraft.

Go ahead and knock out that clearOverlay() function and add a variable to save the current overlay.

var returnOverlay: MKPolyline?

func clearOverlay() {
if let overlay = returnOverlay {
mapView.removeOverlay(overlay)
returnOverlay = nil
}
}

Providing the Renderer

Unlike annotations, the map view doesn’t supply default overlay renderers. To use overlays, you must implement the following delegate function:

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!)image
-> MKOverlayRenderer! {
if overlay === returnOverlay {
let renderer = MKPolylineRenderer(overlay: returnOverlay)
renderer.strokeColor = UIColor(red: 0.4,
green: 1.0,
blue: 0.4,
alpha: 0.7)
renderer.lineCap = kCGLineCapRound
renderer.lineWidth = 16.0
renderer.lineDashPattern = [ 38.0, 22.0 ]
return renderer
}
return nil
}

The code asks whether the map view is requesting the renderer for your return path overlay. If so, create an MKPolylineRenderer object and configure it to draw a light green, semitransparent, dashed line with rounded end caps. Note that there’s no renderer cache because renderer objects are not reusable; there is one renderer object for each overlay object.

CREATING A CUSTOM OVERLAY RENDERER

Creating a custom overlay renderer is (nearly) as easy as creating a custom UIView, just like the one you wrote in Chapter 11. At a minimum, you create a subclass of MKOverlayRenderer and then override the drawMapRect(_:,zoomScale:,inContext:) function.

Like UIView’s drawRect(_:) function, your drawMapRect(...) function is painting into a Core Graphics context. Unlike drawRect(_:), the context is not your own. Your code draws directly into the map view’s context, decorating the map with your overlay.

Your object gets its data from the overlay object associated with the renderer object, which you obtain via the inherited overlay property. If you need to display custom data, you would define your own overlay class and then use that object in your custom renderer. The superclass also provides useful conversion functions for turning map points into graphics coordinates, and vice versa.

Renderer drawing does present one complication. Map view drawing is performed on a background thread, for performance reasons. This means that some UIKit drawing objects and functions can’t be used because they’re not thread safe. They can be avoided by sticking to the Core Graphics functions for all drawing (all of those functions that start with CGContext...()). The documentation for MKOverlayRenderer describes the special steps, and restrictions, needed to draw with UIKit classes.

There’s an example of a custom renderer class in the Homer app, which is available for free in the App Store. You can download the source for Homer at https://github.com/JamesBucanek/Pigeon.

Using the simulator or a provisioned device, run Pigeon, save a location, and then move away from that position. The renderer object will draw a fat dashed line between your current location and the saved one, as shown in Figure 17-10, as long as both are known.

image

Figure 17-10. Testing the overlay renderer

Annotations and overlays provide almost an unlimited means for adding content to your maps. You can highlight points of interest, provide geographic information, draw travel routes, overlay weather information, promote businesses, or display the location of other players—the possibilities go on and on. If you need more than the basic map pins, lines, and regions provided by iOS, you’re free to invent your own annotation views and overlay renderers.

Are you still wondering what the info button in the toolbar is for? I saved that for the exercise at the end of the chapter. Before you get to that, let’s take a brief tour of some location services and map features you haven’t explored yet.

Location Monitoring

Pigeon is the kind of app that uses immediate, precise (as possible), and continuous monitoring of the user’s location. Because of this, it requires an iOS device with GPS capabilities and gathers location data continuously. This isn’t true for all apps. Many apps don’t need precise location information, continuous monitoring, or to be immediately notified of movement.

For apps with less demanding location requirements, the Core Location framework offers a variety of information and delivery methods. Each method involves a different amount of hardware and CPU involvement, which means that each will impact the battery life and performance of the iOS device in varying ways.

As a rule, you want to gather the least amount of location information your app needs to function. Let’s say you’re writing a travel app that needs to know when the user has left one city and arrived at the next. Do not fire up the GPS hardware (the way Pigeon does) and start monitoring their every movement. Why? Because your app will never get the notification that they’ve arrived in their destination city because the user’s battery will have been completely drained! And the first thing the user is going to do, after recharging, is to delete your app. Take a look at some other ways of getting location information that don’t require as much juice.

Approximate Location and Non-GPS Devices

Location information is also available on iOS devices that don’t have GPS hardware. These devices use location information that they gather from Wi-Fi base stations, cell phone towers, and other sources. The accuracy can be crude—sometimes kilometers instead of meters—but it’s enough to place the user in a town. This is more than enough information to suggest restaurants or list movies that are playing in their vicinity.

So, even if you left out the gps hardware requirement for your app, you can still request location information, and you might get it. Consult the horizontalAccuracy property of the CLLocation object for the uncertainty (in meters) of the location’s reported position. If that value is large, then the device probably isn’t using GPS or it’s in a situation where GPS is not accurate.

Note iOS devices with GPS also use this alternative location information to improve the speed of GPS triangulation—which, by itself, is rather slow—and to reduce power consumption. This system is called Assisted GPS.

If your app needs only approximate location information, gather your location data by sending CLLocationManager the startMonitoringSignificantLocationChanges() function instead of the startUpdatingLocation() function. This function gets only a rough estimate of the user’s location and notifies your app only when that location changes in a big way—typically 500 meters or more—saving a great deal of processing power and battery life.

The significant location change service also notifies your app even when it’s not running—something that regular location monitoring (via startUpdatingLocation()) doesn’t do. If you start significant location change updates and your app is subsequently stopped, iOS will relaunch your app (in the background) and notify it of the new location.

Note Using significant location change notifications, or any other location gathering that would occur while your app isn’t running, requires that you request authorization to continuously monitor the user’s location using the requestAlwaysAuthorization() function. If you fail to do this or the user explicitly denies your app permission to gather location data in the background, none of these APIs will do anything useful.

Monitoring Regions

Getting back to that travel app, some iOS devices are capable of monitoring significant changes in location, even when the device is idle. This is accomplished using region monitoring. Region monitoring lets you define an area on the map and be notified when the user enters or exits that region. This is an extremely efficient (low-power) method of determining when the user has moved.

You could, for example, create a region (CLRegion) object encompassing the airport they are traveling to next. You would call the location manager object’s startMonitoringForRegion(_:) function for each region, up to 20. Then all your app has to do is sit back and wait until the delegate object receives a call to locationManager(_:,didEnterRegion:) or locationManager(_:,DidExitRegion:).

Use region monitoring to be notified when the user arrives at work or at their family reunion. To learn more about region monitoring, find the “Monitoring Shape-Based Regions” section of the Location Services and Maps Programming Guide that you’ll find in Xcode’s Documentation and API Reference window. The Location Services and Maps Programming Guide also describes how to receive location data in the background when your app isn’t the active app, something I haven’t discussed in this book.

Reducing Location Change Messages

Another way to stretch battery life is to reduce the amount of location information your app receives. I already talked about receiving only significant changes or monitoring regions, but there’s a middle ground between that extreme and getting second-by-second updates on the user’s location.

The first method is to set the location manager’s distanceFilter and desiredAccuracy properties. The distanceFilter property reduces the number of location updates your app receives. It waits until the device has moved by the set distance before updating your app again. The desiredAccuracy property tells iOS how much effort it should expend trying to determine the user’s exact location. Relaxing that property means the location hardware doesn’t have to work as hard.

Another hint you can provide is the activityType property. This tells the manager that your app is used for automotive navigation, as opposed to being a personal fitness app. The location manager will use this hint to optimize its use of hardware. An automobile navigation app might, for example, temporarily power down the GPS receivers if the user hasn’t moved for an extended period of time.

Movement and Heading

Your app might not be interested so much in where the user is as in what direction they’re going in and how fast. If heading is your game, consult the speed and course properties of the CLLocation object that you obtain from the location property of the CLLocationManager.

If all you want to know is the user’s direction, you can gather just that by calling the startUpdatingHeading() function (instead of startUpdatingLocation()). The user’s heading can be determined somewhat more efficiently than their exact location.

To learn more about direction information, read the “Getting Direction-Related Events” chapter of the Location Awareness Programming Guide.

Geocoding

What if your app is interested in places on the map? It might want to know where a business is located. Or maybe it has a map coordinate and wants to know what’s there.

The process of converting information about locations (business name, address, city, ZIP code) into map coordinates, and vice versa, is called geocoding. Geocoding is a network service, provided by Apple, that will convert a dictionary of place information (say, an address) into a longitude and latitude, and back again, as best as it can. Turning place information into a map coordinate is called forward geocoding. Turning a map coordinate into a description of what’s there is called reverse geocoding.

Geocoding is performed through the CLGeocoder object. CLGeocoder will turn either a dictionary of descriptive information or a map coordinate into a CLPlacemark object. A placemark object is a combination of a map coordinate and a description of what’s at that coordinate. This information will include what country, region, and city the coordinate is in, a street address, and a postal code (if appropriate), even whether it’s a body of water.

Getting Directions

Another resource your app has at its disposal is the Maps app—yes, the standard Maps app that comes with iOS. There are methods that let your app launch the Maps app to assist your user. This is a simple way of providing maps, locations, directions, and navigation services to your user without adding any of that to your app.

You use the Maps app via the MKMapItem object. You create one or more MKMapItem objects either from the current location (mapItemForCurrentLocation()) or from a geocoded placemark object (MKMapItem(placemark:)).

Once created, call the map item object’s openInMapsWithLaunchOptions(_:) function (for one map item) or pass an array of map items to openMapItems(_:,launchOptions:). The launch options are a dictionary that can optionally tell the Maps app what region of the globe to display, a region on the map to highlight, whether you want it provide driving or walking directions to a given location, what mode to use (map, satellite, hybrid), whether to display traffic information, and so on.

Code examples using MKMapItem are shown in the “Providing Directions” chapter of the Location Services and Maps Programming Guide.

If you want to provide directions in your app, use the MKDirectionsRequest class. You start by creating two MKMapItem objects, for the start and end of the trip. You then start an MKDirectionsRequest. This object contacts the map servers and returns one or more possible routes. These routes are described by MKRoute objects. If you want to display one of those routes in your map, get the polyline property of the MKRoute object; it's an MKPolyline overlay object, ready to add to the map, exactly as you did in Pigeon. You can find more details, along with example code, in the “Providing Directions” chapter of the Location Services and Maps Programming Guide.

The Homer app, on the App Store, also performs geocoding and gets directions. You can download the source code for Homer at https://github.com/JamesBucanek/Pigeon.

Summary

You’ve traveled far in your journey toward mastering iOS app development. You’ve passed many milestones, and learning to use location services is a big one. You now know how to get the user’s location, for a variety of purposes, and display that on a map.

Speaking of which, you also learned a lot about maps. You now know how to present a map in your app, annotate it with points of interest, and customize how those annotations look. You learned how to track and display the user’s location on the map and add your own overlays.

But you know what? Pigeon still has the same problem that MyStuff has. What good is an app that’s supposed to remember stuff if it forgets everything when you quit the app? There should be some way of storing its data somewhere so when you come back to the app it hasn’t lost everything. Not surprisingly, there’s a bunch of technologies for saving data, and the next two chapters are devoted to just that.

EXERCISE

MKMapView can display graphic maps, satellite images, or a combination of both. It can orient the map to true north or rotate the map based on the orientation of the device. It’s rude not to let your user choose which of these options they want to use. Pigeon locked the map view’s orientation and display mode. Your exercise is to fix that.

These two aspects of the map display are controlled by two properties: mapType and userTrackingMode. The map type can be set to display graphics (MKMapType.Standard), satellite imagery (MKMapType.Stellite), or a combination of the two (MKMapType.Hybrid). The user’s tracking mode can either follow the user (MKUserTrackingMode.Follow) or follow them with heading (MKUserTrackingMode.FollowWithHeading).

The controls you add to the interface are up to you. Some apps add a button right to the map interface that toggles between different map types and tracking modes. For Pigeon, I decided to place the settings on a separate view controller.

You’ll find the finished project in the Learn iOS Development Projects image Ch 17 image Pigeon E1 folder. Basically, here’s what I did:

1. Create a new Swift class called OptionsViewController, which is a subclass of UIViewController.

2. In the Main.storyboard file, add a new view controller and change the class of the view controller to OptionsViewController.

3. In the new view, add two segmented controls. Configure one to have three segments (Map, Satellite, Hybrid) and the second one to have two segments (North, Heading).

4. Make the background color of the root view slightly tinted and semitransparent.

5. In OptionsViewController, add outlets for the segmented controls and two @IBAction functions to change the settings.

6. Override the viewWillAppear() function and obtain a reference to the MKMapView object from the presentingController. Use the current property values of the map to update the initial selection of the segmented controls.

7. In the two action functions, use the reference to the presenting controller’s map view and change the map type or tracking mode based on selected segment.

8. Add a done() function to dismiss the view controller.

9. Connect the segmented controls to the outlets and connect each sent action to the appropriate OptionsViewController action.

10.Add a single-tap gesture recognizer, attach it to the root view, and connect it to the done() function. This will dismiss the view controller.

11.Create a segue from the info button in the toolbar to the new view controller. Set the seque to Present Modally, the presentation to Over Full Screen, and the transition to Cover Vertically.

This is all stuff you learned in Chapters 10 and 12. Here’s the finished interface in action:

image