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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Routing With MapKit and Core Location
25 mins
- Getting Started
- Using MapKit With CoreLocation
- Getting the User’s Location With CoreLocation
- Autocompleting the User’s Location
- Getting Authorization to Access the User’s Location
- Turning the User’s Coordinates Into an Address
- Processing User Input With MKLocalSearchCompleter
- Improving MKLocalSearchCompleter’s Accuracy
- Finishing the Address’ Autocomplete
- Calculating Routes With MapKit
- Requesting MKRoute Directions
- Rendering Routes Into the Map
- Walking Through Each MKRoute.Step
- Where to Go From Here?
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:
- If the user edited the origin field, you remove the current location.
- You make sure that the query contains information, because it doesn’t make sense to send an empty query to the completer.
- If the field is empty and the completer is currently attempting to find a match, you cancel it.
- 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.
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:
-
commonDelta
refers to the zoom level you want. Increase the value for broader map coverage. - This is the region you created using the coordinates obtained via
CoreLocation
‘s delegate.
Now, build and run.
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.
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:
- Dismiss the keyboard and disable the Calculate Route button.
- Here’s where the current location information comes in handy.
RouteBuilder
handleslocation
andtext
types in different ways.MKLocalSearch
handles the user’s input andCLGeocoder
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. - You map the remaining fields as
text
segments. - Ensure that there’s enough information before showing
DirectionsViewController
. - Build the route with the segments and current region.
- After the helper finishes, re-enable the Calculate Route button.
- 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.
That’s it for ViewControllers/RouteSelectionViewController.swift!
Now, switch to ViewControllers/DirectionsViewController.swift to finish routing.