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 2 of 4 of this article. Click here to view the first page.

Processing User Input With MKLocalSearchCompleter

Still in ViewControllers/RouteSelectionViewController.swift, add another property to the top of the class:

private let completer = MKLocalSearchCompleter()

Here, you use MKLocalSearchCompleter to guess the final address when the user starts typing in the text field.

Add the following to textFieldDidChange(_:) in Actions:

// 1
if field == originTextField && currentPlace != nil {
  currentPlace = nil
  field.text = ""
}
// 2
guard let query = field.contents else {
  hideSuggestionView(animated: true)
  // 3
  if completer.isSearching {
    completer.cancel()
  }
  return
}
// 4
completer.queryFragment = query

Here’s what you’ve done:

  1. If the user edited the origin field, you remove the current location.
  2. You make sure that the query contains information, because it doesn’t make sense to send an empty query to the completer.
  3. If the field is empty and the completer is currently attempting to find a match, you cancel it.
  4. Finally, you pass the user’s input to the completer’s queryFragment.

MKLocalSearchCompleter uses the delegate pattern to surface its results. There’s one method for retrieving the results and one to handle errors that may occur.

At the bottom of the file, add a new extension for MKLocalSearchCompleterDelegate:

// MARK: - MKLocalSearchCompleterDelegate

extension RouteSelectionViewController: MKLocalSearchCompleterDelegate {
  func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
    guard let firstResult = completer.results.first else {
      return
    }
    
    showSuggestion(firstResult.title)
  }

  func completer(
    _ completer: MKLocalSearchCompleter, 
    didFailWithError error: Error
  ) {
    print("Error suggesting a location: \(error.localizedDescription)")
  }
}

MKLocalSearchCompleter returns many results to completerDidUpdateResults(_:), but for this app, you’ll use only the first one. showSuggestion(_:) is a helper method that takes care of adjusting an auto layout constraint and setting a label’s text property.

The last thing to do before the completer will work is to hook up the delegate. Inside viewDidLoad() add:

completer.delegate = self

Build and run. Now, as you type in any of the text fields, a little view will slide in from the bottom with a suggestion.

RWRouter initial screen with suggestion view

Improving MKLocalSearchCompleter’s Accuracy

You may notice that the results from the completer don’t make sense given your location. This is because you never told the completer what your current location is.

Before connecting the completer to the current location, add the final few properties to the top of the class:

private var editingTextField: UITextField?
private var currentRegion: MKCoordinateRegion?

You’ll use the text field property to manage which field is currently active when the user taps a suggestion. You store a reference to the current region that MKLocalSearch will use later to determine where the user currently is.

Toward the bottom of the file, replace // TODO: Configure MKLocalSearchCompleter here… with the following:

//1
let commonDelta: CLLocationDegrees = 25 / 111
let span = MKCoordinateSpan(
  latitudeDelta: commonDelta, 
  longitudeDelta: commonDelta)
//2
let region = MKCoordinateRegion(center: firstLocation.coordinate, span: span)
currentRegion = region
completer.region = region

Here’s what this code does:

  1. commonDelta refers to the zoom level you want. Increase the value for broader map coverage.
  2. This is the region you created using the coordinates obtained via CoreLocation‘s delegate.
Note: 1 degree of latitude is approximately equal to 111 kilometers.

Now, build and run.

RWRouter initial screen with a more precise suggestion view

Finishing the Address’ Autocomplete

Although you can get localized results based on your input, wouldn’t it be nice to select that suggestion? Of course it would — and there’s already a method defined that lets you do so!

Add the following inside suggestionTapped(_:):

hideSuggestionView(animated: true)

editingTextField?.text = suggestionLabel.text
editingTextField = nil

When the user taps the suggestion label, the view collapses and its text is set to the active text field. Before this will work, however, you need to set editingTextField.

To do that, add these few lines to textFieldDidBeginEditing(_:):

hideSuggestionView(animated: true)

if completer.isSearching {
  completer.cancel()
}

editingTextField = textField

This code makes sure that when the user activates a text field, the previous suggestion no longer applies. This also applies to the completer, which you reset if it’s currently searching. Finally, you set the text field property that you defined earlier.

Build and run.

Animation of a user tapping the suggestion view

Suggesting locations and requesting the user’s location both work. Great job!

The final two pieces to this puzzle are calculating a route and displaying that route’s information.

Calculating Routes With MapKit

There’s one thing left to do in ViewControllers/RouteSelectionViewController.swift: You need a way to manage the user’s input and pass it to ViewControllers/DirectionsViewController.swift.

Fortunately, the struct in Models/Route.swift already has that ability. It has two properties: one for the origin and another for the stops along the way.

Instead of creating a Route directly, you’ll use the aids in Helpers/RouteBuilder.swift. This does the busy work of transforming the user’s input for you. For brevity, this tutorial won’t dive into the inner workings of this file. If you’re interested in how it works, however, discuss it in the comments below!

Back in ViewControllers/RouteSelectionViewController.swift, add the following to calculateButtonTapped():

// 1
view.endEditing(true)

calculateButton.isEnabled = false
activityIndicatorView.startAnimating()

// 2
let segment: RouteBuilder.Segment?
if let currentLocation = currentPlace?.location {
  segment = .location(currentLocation)
} else if let originValue = originTextField.contents {
  segment = .text(originValue)
} else {
  segment = nil
}

// 3
let stopSegments: [RouteBuilder.Segment] = [
  stopTextField.contents,
  extraStopTextField.contents
]
.compactMap { contents in
  if let value = contents {
    return .text(value)
  } else {
    return nil
  }
}

// 4
guard 
  let originSegment = segment, 
  !stopSegments.isEmpty 
  else {
    presentAlert(message: "Please select an origin and at least 1 stop.")
    activityIndicatorView.stopAnimating()
    calculateButton.isEnabled = true
    return
}

// 5
RouteBuilder.buildRoute(
  origin: originSegment,
  stops: stopSegments,
  within: currentRegion
) { result in
  // 6
  self.calculateButton.isEnabled = true
  self.activityIndicatorView.stopAnimating()

  // 7
  switch result {
  case .success(let route):
    let viewController = DirectionsViewController(route: route)
    self.present(viewController, animated: true)
    
  case .failure(let error):
    let errorMessage: String
  
    switch error {
    case .invalidSegment(let reason):
      errorMessage = "There was an error with: \(reason)."
    }
    
    self.presentAlert(message: errorMessage)
  }
}

This is what’s happening, step by step:

  1. Dismiss the keyboard and disable the Calculate Route button.
  2. Here’s where the current location information comes in handy. RouteBuilder handles location and text types in different ways. MKLocalSearch handles the user’s input and CLGeocoder handles the location’s coordinates. You only use the current location for the origin of the route. This makes the first segment the one that needs special treatment.
  3. You map the remaining fields as text segments.
  4. Ensure that there’s enough information before showing DirectionsViewController.
  5. Build the route with the segments and current region.
  6. After the helper finishes, re-enable the Calculate Route button.
  7. Show DirectionsViewController if everything went as planned. If something went wrong, show the user an alert explaining what happened.

A lot is going on here. Each part accomplishes something small.

Build and run, then try out a few stops. MKLocalSearch makes this fun on your phone, too, since it works based on what’s around you.

Location pins on a map

That’s it for ViewControllers/RouteSelectionViewController.swift!

Now, switch to ViewControllers/DirectionsViewController.swift to finish routing.