© Jeffrey Linwood 2020
J. LinwoodBuild Location Apps on iOS with Swifthttps://doi.org/10.1007/978-1-4842-6083-8_5

5. Getting Directions with MapKit

Jeffrey Linwood1 
(1)
Austin, TX, USA
 

Providing walking or driving directions from one place to another inside your application is easy with MapKit. You can also instruct the Apple Maps app to open up and route the user to a location – this could be useful if you think the user might just prefer to use a dedicated app.

Create another Swift iOS project in Xcode with the Single View Application template. Name your application DirectionsMapKitApp. Go ahead and add a map view to the only screen in the storyboard, and connect it to an outlet called mapView on the ViewController class . In the view controller, import the MapKit framework, along with UIKit.

You can also reuse one of the projects from an earlier chapter if you like, as those will be set up with the map view.

Understanding the Directions API

Similar to local search, the directions API isn’t a publicly available REST API. Instead, the Apple directions functionality is accessed through MapKit classes . If you happen to have a web version of your application, Apple also provides directions through MapKit for JavaScript, but that product is outside the scope of this book.

To use the directions API, you will use an instance of the MKDirections class. Initialize that instance with an MKDirections.Request object that specifies which directions you want to request, and then make an asynchronous call to the Apple servers with either the calculate() or calculateETA() method. The calculate() method takes a completion handler to use as a callback with either an MKDirections.Response object or an Error object.

The response will contain one or more routes to follow. Typically, you might just want to request one route, but you can request alternate routes in your directions request – doing this will give you multiple routes if there are any.

These routes are MKRoute objects , and they contain several things that would be useful for displaying routes to a user – the route geometry, which can be added to the map view as an overlay, a list of route steps (as MKRouteStep objects), the total distance, and the expected travel time.

Let’s illustrate how these classes work together with a simple example for driving directions.

Getting started with directions

We’ll need to start by creating a directions request object. Create a new method named getDirections() in your view controller class, and then place a call to that method in your viewDidLoad() method . Add the following line of code to the getDirections() method to create a directions request object:
let request = MKDirections.Request()

All of the code from this section of the chapter is listed out in full in Listing 5-1.

MKDirections.Request can take a source and a destination – both of which need to be MKMapItem objects. We discussed the MKMapItem class in Chapter 4. You can certainly use an MKMapItem instance that comes from a local search, or you can create your own MKMapItem objects from an MKPlacemark instance. Placemarks can be created from Core Location coordinates.

We’ll need to do this for both the source and the destination. Because there is some repeatable code, we can abstract this out to a helper function:
func createMapItem(latitude:Double,
  longitude:Double) -> MKMapItem {
  let coord = CLLocationCoordinate2D(
    latitude: latitude,
    longitude: longitude)
  let placemark = MKPlacemark(coordinate: coord)
  return MKMapItem(placemark: placemark)
}
Once we have this helper function, creating map items for the source and destination properties looks like the following code, which you can add to the getDirections() method :
request.source = createMapItem(latitude: 40.7128, longitude: -74)
request.destination = createMapItem(latitude: 38.91, longitude: -77.037)

These coordinates happen to be for New York City (as the source) and Washington, DC (as the destination). Feel free to replace these with your own start and end points, as long as it’s feasible to drive between them.

The directions request takes a transportation type for the request. These direction types are enumerated in the MKDirectionsTransportType struct and include
  • MKDirectionsTransportType.automobile

  • MKDirectionsTransportType.walking

  • MKDirectionsTransportType.transit

  • MKDirectionsTransportType.any

Typically, you would specify one of automobile, walking, or transit, similar to the Apple Maps iOS app. In your getDirections() function, add the following line of code:
request.transportType = .automobile
After setting up the directions request with a source, direction, and transport type, you can create an MKDirections object from the directions request:
let directions = MKDirections(request: request)
Next, you can call the calculate() or the calculateETA() method on the MKDirections object. Both are asynchronous and take a completion handler as the only argument. In our case, we are interested in getting the complete results, including steps, a graphic for the route, and the time and distance, so we will use the calculate() method . The following code sets us up by unwrapping the response:
directions.calculate { (response, error) in
  guard let response = response else {
    print(error ?? "No error found")
    return
  }
}

We’ll pass a closure to the calculate() method. The closure takes two arguments – an optional MKDirections.Response and an optional Error. We’ll check to make sure that the response exists with a guard let statement and then handle the error. This code simply prints out the error, but you could present it to the user in an alert view.

If we do have a valid response, one easy thing to do with the response is to display the route on the map view. The route is an MKPolyline , which can be added to the map view as an overlay. You will also need to add a renderer for the overlay – this will turn the polyline into a graphic. Let’s start by adding the route to the map:
let route = response.routes[0]
self.mapView.addOverlay(route.polyline)
The next step is to make sure that the map displays the route in its window. We’ll also include some padding, so that the start and end of the route aren’t right on the edges of the screen. The polyline has a bounding map rectangle property that you can use to set the visible map rectangle, along with the edge padding.
let padding = UIEdgeInsets(
  top: 40, left: 40, bottom: 40, right: 40)
self.mapView.setVisibleMapRect(
  route.polyline.boundingMapRect,
  edgePadding: padding,
  animated: true)
The last piece of the puzzle is the renderer for the map view overlays. You will need to implement the MKMapViewDelegate protocol in your ViewController class, as well as set the delegate on the map view to be the view controller, like this:
class ViewController: UIViewController, MKMapViewDelegate {
  @IBOutlet weak var mapView: MKMapView!
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    mapView.delegate = self
  }
}
To get the overlay to show up, use the mapView(_ mapView:, rendererFor overlay:) method in the MKMapViewDelegate protocol . This method returns an MKOverlayRenderer or one of its subclasses, such as MKPolylineRenderer:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
  let renderer = MKPolylineRenderer(overlay: overlay)
  renderer.strokeColor = .red
  return renderer
}

Here, we simply create an MKPolylineRenderer for the overlay and then set the stroke color for the line to be red. There is no fill color for a polyline renderer, just stroke.

If we put the code to create the directions request into a function named getDirections() and then call that method from viewDidLoad(), we will end up with a class that looks like Listing 5-1.
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate {
  @IBOutlet weak var mapView: MKMapView!
  override func viewDidLoad() {
    super.viewDidLoad()
    mapView.delegate = self
    getDirections()
  }
  func getDirections() {
    let request = MKDirections.Request()
    request.source = createMapItem(
      latitude: 40.7128, longitude: -74)
    request.destination = createMapItem(
      latitude: 38.91, longitude: -77.037)
    request.transportType = .automobile
    let directions = MKDirections(request: request)
    directions.calculate { (response, error) in
        guard let response = response else {
            print(error ?? "No error found")
            return
        }
        let route = response.routes[0]
        let padding = UIEdgeInsets(
          top: 40, left: 40, bottom: 40, right: 40)
        self.mapView.addOverlay(route.polyline)
        self.mapView.setVisibleMapRect(
          route.polyline.boundingMapRect,
          edgePadding: padding,
          animated: true)
    }
  }
  func createMapItem(latitude:Double, longitude:Double) -> MKMapItem {
    let coord = CLLocationCoordinate2D(
      latitude: latitude,
       longitude: longitude)
    let placemark = MKPlacemark(coordinate: coord)
    return MKMapItem(placemark: placemark)
  }
  func mapView(_ mapView: MKMapView,
    rendererFor overlay: MKOverlay) ->
    MKOverlayRenderer {
    let renderer = MKPolylineRenderer(
      overlay: overlay)
    renderer.strokeColor = .red
    return renderer
  }
}
Listing 5-1

Complete ViewController class for displaying a route

After running this application in the Simulator, you should expect to see a screen similar to Figure 5-1.
../images/485790_1_En_5_Chapter/485790_1_En_5_Fig1_HTML.jpg
Figure 5-1

Displaying driving directions as an overlay on a map view

Understanding route steps

In addition to the route graphic that you can use as an overlay on the map view, the directions API will also return a set of steps for each route on the response. These steps are MKRoute.Step objects. Each route step is a single, discrete action that the user would take to follow the directions, such as walking down a street for 400 meters or driving down a road for 30 kilometers. All distances are given in meters, so you will probably want to convert to kilometers for longer distances or feet and miles for the United States. Each step also includes the geometry associated with that step, so that you could overlay it on a map view. The properties on the MKRoute.Step class are
  • polyline – The geometry for this route step

  • instructions – The verbal directions to follow

  • notice – Optional legal information or warnings associated with this step

  • distance – The distance in meters

  • transportType – One of the values from MKDirectionsTransportType (automobile, transit, walking)

If you wanted to simply iterate through each step in the route and print to console, you could use a function similar to this:
func printRouteSteps(_ route:MKRoute) {
  for step in route.steps {
    print("Go (step.distance) meters")
    print(step.instructions)
  }
}

Let’s take this a few steps further and build step-by-step directions into our application.

Building step-by-step directions

We’ll modify the view controller we’ve already built, but you could certainly put step-by-step directions on its own screen. The first step we’ll take will be to create the user interface elements for step-by-step directions:
  • Previous and Next Step buttons to move between steps – These have black backgrounds, with white text for the normal state and light gray for the disabled state. Both should initially be disabled.

  • An instructions label – This is multiple lines, to accommodate longer directions. It has a dark gray background with white text.

  • A notice label – This label has a red background with white text, to separate it from the instructions label.

  • A distance label – This label has a 0.8 alpha component, to give a little visual separation from the adjacent buttons.

Go ahead and add these user interface elements to the storyboard. You can arrange them however you like. You can also use Auto Layout to make sure the elements properly scale across all devices. The sample project for the book has the user interface elements at the bottom of the storyboard, with the buttons in line with the distance label, and the instructions label and notice label underneath that.

You should have something similar to Figure 5-2.
../images/485790_1_En_5_Chapter/485790_1_En_5_Fig2_HTML.jpg
Figure 5-2

The user interface elements for the step-by-step directions

Create outlets for each user interface element, and use these names:
  • previousButton

  • nextButton

  • instructionsLabel

  • noticeLabel

  • distanceLabel

Also create actions for both of the buttons, named previous and next. We will incorporate code into each of these methods to change the displayed step.

Last, add instance variables to the view controller to keep track of the current state. We will need to store the current route and the current step index. We will store the current route as an optional and initialize the current step index to zero:
var currentRoute:MKRoute?
var currentStepIndex = 0

Displaying the current step

One way to organize your step-by-step directions code is to create a function that is responsible for displaying the current step. This function will need to set the content of each label, disable or enable the previous and next buttons based on index, and update the map to display the current route segment.

Create a method named displayCurrentStep(). Inside this function, we are going to do some basic checking to make sure that we have a route, and the current step index is within the bounds of the route’s steps array. The complete version of the displayCurrentStep() method is in Listing 5-2:
func displayCurrentStep() {
  guard let currentRoute = currentRoute else {
      return
  }
  if (currentStepIndex >= currentRoute.steps.count) {
      return
  }
}
Let’s add some more functionality to this method. Continue to add these statements to displayCurrentStep(), after the checks. We’ll keep track of the current step and then use that to set the properties of each of the labels:
let step = currentRoute.steps[currentStepIndex]
instructionsLabel.text = step.instructions
distanceLabel.text = "(step.distance) meters"
We could add some functionality here for the distance label to change units if we wanted to. For the notice, not every route step will have a notice, so if there is not a notice (it is an optional), hide the notice label. Conversely, be sure to show the notice label if it is there:
if step.notice != nil {
    noticeLabel.isHidden = false
    noticeLabel.text = step.notice
} else {
    noticeLabel.isHidden = true
}
The next step is to manage the state of the buttons. The previous button should be disabled if the current step index is zero or less. The next button should be disabled if the current step index is the number of steps – 1 or more:
previousButton.isEnabled = currentStepIndex > 0
nextButton.isEnabled = currentStepIndex < (currentRoute.steps.count - 1)
We still need to add the actions for each of these buttons, but let’s finish out the step display by showing the polyline for the route step in the map view. Similar to what we did for the entire route, we are going to set the visible map rectangle based on the bounding map rectangle of the polyline. We’ll also use the same padding on the edges to keep the segments visible. Add the following code to the end of your displayCurrentStep() method :
let padding = UIEdgeInsets(
  top: 40, left: 40, bottom: 40, right: 40)
mapView.setVisibleMapRect(
  step.polyline.boundingMapRect,
  edgePadding: padding,
  animated: true)

Now that we have all of the display code, we need two more pieces – saving the route in the completion handler for the directions API and the actions for the previous and next actions.

Saving the route in an instance variable

When we get the directions response from the API in the getDirections() method, we will need to store the route as an instance variable. Inside the completion handler, save the route to the currentRoute instance variable:
directions.calculate { (response, error) in
  guard let response = response else {
      print(error ?? "No error found")
      return
  }
  let route = response.routes[0]
  self.currentRoute = route
  self.displayCurrentStep()
  // Additional code follows
}

After the route has been saved, calling the displayCurrentStep() method we created in the previous section will initialize the step-by-step directions user interface with the current step.

Now, we need to add functionality to the actions for the previous and next buttons.

Actions for the previous and next buttons

The actions for the previous and next buttons will update the current step index. We will need to check that the current route is set first. We also then make sure that changing the step index stays within the bounds of the steps array.

For the previous() action, if the current step index is zero or less, the method will simply return:
@IBAction func previous(_ sender: Any) {
  if currentRoute == nil {
    return
  }
  if (currentStepIndex <= 0) {
    return
  }
  currentStepIndex -= 1
  displayCurrentStep()
}
The next() action is similar, except that the check is for whether the current step index is less than the number of steps – 1:
@IBAction func next(_ sender: Any) {
  guard let currentRoute = currentRoute else {
    return
  }
  if (currentStepIndex >=
    (currentRoute.steps.count - 1)) {
    return
  }
  currentStepIndex += 1
  displayCurrentStep()
}
After filling in the bodies for each of these actions, your application should be able to retrieve step-by-step directions. After running the application and clicking the Next button, you should see a screen like Figure 5-3.
../images/485790_1_En_5_Chapter/485790_1_En_5_Fig3_HTML.jpg
Figure 5-3

One step in the step-by-step directions

The complete code for the view controller is in Listing 5-2, with all of the methods and instance variables.
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate {
  @IBOutlet weak var mapView: MKMapView!
  @IBOutlet weak var previousButton: UIButton!
  @IBOutlet weak var nextButton: UIButton!
  @IBOutlet weak var distanceLabel: UILabel!
  @IBOutlet weak var instructionsLabel: UILabel!
  @IBOutlet weak var noticeLabel: UILabel!
  var currentRoute:MKRoute?
  var currentStepIndex = 0
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    mapView.delegate = self
    getDirections()
  }
  func getDirections() {
    let request = MKDirections.Request()
    // New York City
    request.source = createMapItem(latitude: 40.7128, longitude: -74)
    // Washington, DC
    request.destination = createMapItem(latitude: 38.91, longitude: -77.037)
    request.transportType = .automobile
    let directions = MKDirections(request: request)
    directions.calculate { (response, error) in
      guard let response = response else {
        print(error ?? "No error found")
        return
      }
      let route = response.routes[0]
      self.currentRoute = route
      self.displayCurrentStep()
      let padding = UIEdgeInsets(top: 40, left: 40, bottom: 40, right: 40)
      self.mapView.addOverlay(route.polyline)
      self.mapView.setVisibleMapRect(
        route.polyline.boundingMapRect,
        edgePadding: padding,
        animated: true)
    }
  }
  func displayRouteSteps(_ route:MKRoute) {
    for step in route.steps {
      print("Go (step.distance) meters")
      print(step.instructions)
    }
  }
  func createMapItem(latitude:Double, longitude:Double) -> MKMapItem {
    let coord = CLLocationCoordinate2D(latitude: latitude,
                            longitude: longitude)
    let placemark = MKPlacemark(coordinate: coord)
    return MKMapItem(placemark: placemark)
  }
  func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    let renderer = MKPolylineRenderer(overlay: overlay)
    renderer.strokeColor = .red
    return renderer
  }
  @IBAction func previous(_ sender: Any) {
    if currentRoute == nil {
      return
    }
    if (currentStepIndex <= 0) {
      return
    }
    currentStepIndex -= 1
    displayCurrentStep()
  }
  @IBAction func next(_ sender: Any) {
    guard let currentRoute = currentRoute else {
      return
    }
    if (currentStepIndex >= (currentRoute.steps.count - 1)) {
      return
    }
    currentStepIndex += 1
    displayCurrentStep()
  }
  func displayCurrentStep() {
    guard let currentRoute = currentRoute else {
      return
    }
    if (currentStepIndex >= currentRoute.steps.count) {
      return
    }
    let step = currentRoute.steps[currentStepIndex]
    instructionsLabel.text = step.instructions
    distanceLabel.text = "(step.distance) meters"
    if step.notice != nil {
      noticeLabel.isHidden = false
      noticeLabel.text = step.notice
    } else {
      noticeLabel.isHidden = true
    }
    previousButton.isEnabled = currentStepIndex > 0
    nextButton.isEnabled = currentStepIndex < (currentRoute.steps.count - 1)
    let padding = UIEdgeInsets(top: 40, left: 40, bottom: 40, right: 40)
mapView.setVisibleMapRect(step.polyline.boundingMapRect,
    edgePadding: padding,
    animated: true)
  }
}
Listing 5-2

ViewController class with step-by-step directions functionality

Next steps

There are some improvements to be made to this code – you could add a function that converts the distance to the correct units, or you could allow the user to switch between different transportation types (automobile, transit, or walking).

You could also combine this driving directions code with the local search from the previous chapter to allow users to choose where they want to go for directions.

Summary

Now that you have some experience with Apple’s map directions, you can see how you might use this inside your own app, instead of sending the user to another application such as Apple Maps or Google Maps.

Chapter 6 switches focus away from maps, points of interest, and directions to geofencing. We will work with the region monitoring APIs in the CoreLocation framework to detect when a user enters or exits a given area, so that we can do something inside the app.

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

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