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?
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 aMKRoute
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.
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:
- This is the condition that breaks out of the recursive loop. Without this, the app will suffer the fate of an infinite loop.
- 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. - You configure the request to use the selected tuple value for the source and destination.
- Once you configure the request, you use an instance of
MKDirections
to calculate the directions. - 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!
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:
- Update the class properties
totalDistance
andtotalTravelTime
to reflect the total distance and time of the whole route. - Apply that information to
informationLabel
at the top of the view. - 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.
Congratulations on completing your app. At this point, you should have a good foundation of how a map-based app works.