Getting Directions in MapKit with SwiftUI

Getting Directions in MapKit with SwiftUI

Learn how to get directions and show a route on a Map view in SwiftUI.

A typical scenario could be to calculate travel time and directions for any of the selected search results. Based on a selectedResult we can calculate a route of type MKRoute within a getDirections() function.

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var selectedResult: MKMapItem?
    @State private var route: MKRoute?

    var body: some View {
        Map(selection: $selectedResult) {
            // ...
        }
    }

    func getDirections() { 
        
    }
}

Initial set-up of your view.

Configure a MKDirections.Request() and with source location and destination location. In this example, we are using a fixed location as the source location, but of course, this could be the user's current location or any other coordinate.

func getDirections() {
    self.route = nil
    
    // Check if there is a selected result
    guard let selectedResult else { return }
    
    // Coordinate to use as a starting point for the example
    let startingPoint = CLLocationCoordinate2D(
        latitude: 40.83657722488077,
        longitude: 14.306896671048852
    )
    
    // Create and configure the request
    let request = MKDirections.Request()
    request.source = MKMapItem(placemark: MKPlacemark(coordinate: startingPoint))
    request.destination = self.selectedResult
    
    // Get the directions based on the request
    Task {
        let directions = MKDirections(request: request)
        let response = try? await directions.calculate()
        route = response?.routes.first
    }
}

The request is run asynchronously. Once you get a response you can access the valid route objects and then add them to a property in your view, so you can use them in your interface. Here we have a property called route that will store the first valid route from our response object.

Let’s use the .onChange modifier to monitor the selectedResult property and whenever it changes, we call the getDirections() function.

struct ContentView: View {

    ...
    
    var body: some View {
        Map(selection: $selectedResult) {
            // ...
        }
        .onChange(of: selectedResult){
            getDirections()
        }
    }
    
    ...
}

Getting the direction once a location is selected.

Lastly, in our Map view, we can add a MapPolyLine object if a route is available. We will use a blue stroke to display the way to the selected point of interest.

struct ContentView: View {

    @State private var selectedResult: MKMapItem?
    @State private var route: MKRoute?

    private let startingPoint = CLLocationCoordinate2D(
            latitude: 40.83657722488077,
            longitude: 14.306896671048852
    )

    var body: some View {
        Map(selection: $selectedResult) {
            // Adding the marker for the starting point
            Marker("Start", coordinate: self.startingPoint)
        
            // Show the route if it is available
            if let route {
                MapPolyline(route)
                    .stroke(.blue, lineWidth: 5)
            }
        }
        .onChange(of: selectedResult){
            getDirections()
        }
    }
		
    ...
}

Adding the polyline on the map to represent the route

You can also get other types of information from a route, like the travel time between the source and destination locations. Let’s create a computed property called travelTime. It uses the route object to read and return its expected travel time through the expectedTravelTime property. To get it in the format of hours and minutes use a formatter object.

struct ContentView: View {
    @State private var selectedResult: MKMapItem?
    @State private var route: MKRoute?
    
    private var travelTime: String? {
        // Check if there is a route to get the info from
        guard let route else { return nil }
    
        // Set up a date formater
        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = .abbreviated
        formatter.allowedUnits = [.hour, .minute]
    
        // Format the travel time to the format you want to display it
        return formatter.string(from: route.expectedTravelTime)
    }
    
    var body: some View { ... }
    
    func getDirections() { ... }
}

Creating the computed property to read the travel time of the route.

Here is a completely functional example showing the route between two hard-coded points on a Map view:

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var selectedResult: MKMapItem?
    @State private var route: MKRoute?
    
    private let startingPoint = CLLocationCoordinate2D(
        latitude: 40.83657722488077,
        longitude: 14.306896671048852
    )
    
    private let destinationCoordinates = CLLocationCoordinate2D(
        latitude: 40.849761,
        longitude: 14.263364
    )
    
    var body: some View {
        Map(selection: $selectedResult) {
            // Adding the marker for the starting point
            Marker("Start", coordinate: self.startingPoint)
            
            // Show the route if it is available
            if let route {
                MapPolyline(route)
                    .stroke(.blue, lineWidth: 5)
            }
        }
        .onChange(of: selectedResult){
            getDirections()
        }
        .onAppear {
            self.selectedResult = MKMapItem(placemark: MKPlacemark(coordinate: self.destinationCoordinates))
        }
    }
    
    func getDirections() {
        self.route = nil
        
        // Check if there is a selected result
        guard let selectedResult else { return }
        
        // Create and configure the request
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: self.startingPoint))
        request.destination = self.selectedResult
        
        // Get the directions based on the request
        Task {
            let directions = MKDirections(request: request)
            let response = try? await directions.calculate()
            route = response?.routes.first
        }
    }
    
}

Fully functioning example. Copy and paste in an app project to see it working.