Routing With MapKit and Core Location

Learn how to use MapKit and CoreLocation to help users with address completion and route visualization using multiple addresses. By Ryan Ackermann.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Requesting MKRoute Directions

You can already see a map with the locations you picked, but the routes and directions are missing. You still need to do the following things:

  • Group the Route segments to link them together.
  • For each group, create a MKDirections.Request to get a MKRoute to display.
  • As each request finishes, refresh the map and table views to reflect the new data.

To start chipping away at this list, add this property to the top of the class:

private var groupedRoutes: [(startItem: MKMapItem, endItem: MKMapItem)] = []

This array holds the routes that you request and display in the view. To populate this array with content, add the following to groupAndRequestDirections():

guard let firstStop = route.stops.first else {
  return
}

groupedRoutes.append((route.origin, firstStop))

if route.stops.count == 2 {
  let secondStop = route.stops[1]

  groupedRoutes.append((firstStop, secondStop))
  groupedRoutes.append((secondStop, route.origin))
}

fetchNextRoute()

This method is specific to this app. It creates an array of tuples that hold a start and end MKMapItem. After double-checking the data is valid and there’s more than one stop, you add the origin and first stop. Then, if there’s an extra stop, you add two more groups. The last group is the return trip, ending up back at the start.

Now that you’ve grouped the routes into distinct start and end points, they’re ready for you to feed them into a directions request.

Since there may be many routes to request, you’ll use recursion to iterate through the routes. If you’re unfamiliar with this concept, it’s like a while loop. The main difference is that the recursive method will call itself when it finishes its task.

Note: If you’re interested in learning more, read weheartswift.com’s recursion article.

To see this in action, add the following to fetchNextRoute():

// 1
guard !groupedRoutes.isEmpty else {
  activityIndicatorView.stopAnimating()
  return
}

// 2
let nextGroup = groupedRoutes.removeFirst()
let request = MKDirections.Request()

// 3
request.source = nextGroup.startItem
request.destination = nextGroup.endItem

let directions = MKDirections(request: request)

// 4
directions.calculate { response, error in
  guard let mapRoute = response?.routes.first else {
    self.informationLabel.text = error?.localizedDescription
    self.activityIndicatorView.stopAnimating()
    return
  }

  // 5
  self.updateView(with: mapRoute)
  self.fetchNextRoute()
}

Here’s what this code does:

  1. This is the condition that breaks out of the recursive loop. Without this, the app will suffer the fate of an infinite loop.
  2. To work toward the break-out condition, you need to mutate groupedRoutes. Here, the group that you’ll request is the first in the array.
  3. You configure the request to use the selected tuple value for the source and destination.
  4. Once you configure the request, you use an instance of MKDirections to calculate the directions.
  5. If all went well with the request, you update the view with the new route information and request the next segment of the route.

fetchNextRoute() will continue to call itself after calculate(completionHandler:) finishes. This allows the app to show new information after the user requests each part of the route.

Rendering Routes Into the Map

To begin showing this new information, add the following to updateView(with:):

let padding: CGFloat = 8
mapView.addOverlay(mapRoute.polyline)
mapView.setVisibleMapRect(
  mapView.visibleMapRect.union(
    mapRoute.polyline.boundingMapRect
  ),
  edgePadding: UIEdgeInsets(
    top: 0,
    left: padding,
    bottom: padding,
    right: padding
  ),
  animated: true
)

// TODO: Update the header and table view...

MKRoute provides some interesting information to work with. polyline contains the points along the route that are ready to display in a map view. You can add this directly to the map view because polyline inherits from MKOverlay.

Next, you update the visible region of the map view to make sure that the new information you’ve added to the map is in view.

This isn’t enough to get the route to show up on the map view. You need MKMapViewDelegate to configure how the map view will draw the line displaying the route.

Add the this to the bottom of the file:

// MARK: - MKMapViewDelegate

extension DirectionsViewController: MKMapViewDelegate {
  func mapView(
    _ mapView: MKMapView, 
    rendererFor overlay: MKOverlay
  ) -> MKOverlayRenderer {
    let renderer = MKPolylineRenderer(overlay: overlay)

    renderer.strokeColor = .systemBlue
    renderer.lineWidth = 3
    
    return renderer
  }
}

This delegate method allows you to precisely define how the rendered line will look. For this app, you use the familiar blue color to represent the route.

Finally, to let the map view use this delegate implementation, add this line to viewDidLoad():

mapView.delegate = self

Build and run. You’ll now see lines drawn between the points along with your destinations. Looking good!

Animation showing routes between locations

You almost have a complete app. In the next section, you’ll complete the final steps to make your app fun and effective to use by adding directions.

Walking Through Each MKRoute.Step

At the point, you’ve already implemented most of the table view’s data source. However, the tableView(_:cellForRowAt:) is only stubbed out. You’ll complete it next

Replace the current contents of tableView(_:cellForRowAt:) with:

let cell = { () -> UITableViewCell in
  guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) 
  else {
    let cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
    cell.selectionStyle = .none
    return cell
  }
  return cell
}()

let route = mapRoutes[indexPath.section]
let step = route.steps[indexPath.row + 1]

cell.textLabel?.text = "\(indexPath.row + 1): \(step.notice ?? step.instructions)"
cell.detailTextLabel?.text = distanceFormatter.string(
  fromDistance: step.distance
)

return cell

Two main things are going on here. First, you set the cell’s textLabel using a MKRoute.Step. Each step has instructions on where to go as well as occasional notices to warn a user about hazards along the way.

Additionally, the distance of a step is useful when reading through a route. This makes it possible to tell the driver of a car: “Turn right on Main Street in two miles”. You format the distance using MKDistanceFormatter, which you declare at the top of this class.

Add the final bit of code, replacing // TODO: Update the header and table view… in updateView(with:):

// 1
totalDistance += mapRoute.distance
totalTravelTime += mapRoute.expectedTravelTime

// 2
let informationComponents = [
  totalTravelTime.formatted,
  "• \(distanceFormatter.string(fromDistance: totalDistance))"
]
informationLabel.text = informationComponents.joined(separator: " ")

// 3
mapRoutes.append(mapRoute)
tableView.reloadData()

With this code, you:

  1. Update the class properties totalDistance and totalTravelTime to reflect the total distance and time of the whole route.
  2. Apply that information to informationLabel at the top of the view.
  3. After you add the route to the array, reload the table view to reflect the new information.

Build and run. Awesome! You can now see a detailed map view with turn-by-turn directions between each stop.

Full app running

Congratulations on completing your app. At this point, you should have a good foundation of how a map-based app works.