How to Get Directions and Draw Routes on Maps in iOS Using MKDirections

Get directions and draw routes on maps in iOS using the MKDirections API from the MapKit framework. MKDirections contacts Apple's servers to compute driving and walking routes between two locations. You create an MKDirections.Request with source and destination MKMapItem objects, then call calculate(completionHandler:). The response contains MKRoute objects with polyline overlays and step-by-step instructions.

How to Request User Location Permission

Location services require user permission. Add the following key to Info.plist:

Privacy - Location When in Use Usage Description: We need your location to compute the route.

Declare a CLLocationManager in your view controller:

let locationManager = CLLocationManager()

Initialize in viewDidLoad:

locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self

requestWhenInUseAuthorization asks for permission only while the app is active. Use requestAlwaysAuthorization only for background location tracking.

Implement the CLLocationManagerDelegate method:

extension MapViewController: CLLocationManagerDelegate {
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        if manager.authorizationStatus == .authorizedWhenInUse {
            mapView.showsUserLocation = true
        }
    }
}

showsUserLocation tells the map to display the user position as a blue dot. When location services are denied, the blue dot does not appear but the app continues functioning.

How to Compute a Route with MKDirections

Store the destination placemark when the user selects a location:

var currentPlacemark: CLPlacemark?

Create the method for requesting directions:

@IBAction func showDirection(sender: AnyObject) {
    guard let currentPlacemark = currentPlacemark else { return }
    
    let directionRequest = MKDirections.Request()
    directionRequest.source = MKMapItem.forCurrentLocation()
    
    let destinationPlacemark = MKPlacemark(placemark: currentPlacemark)
    directionRequest.destination = MKMapItem(placemark: destinationPlacemark)
    directionRequest.transportType = .automobile
    
    let directions = MKDirections(request: directionRequest)
    directions.calculate { routeResponse, routeError in
        guard let routeResponse = routeResponse else {
            if let routeError = routeError {
                print("Error: (routeError)")
            }
            return
        }
        
        let route = routeResponse.routes[0]
        self.mapView.addOverlay(route.polyline, level: .aboveRoads)
    }
}

MKDirections.Request configures the source, destination, and transport type. MKMapItem.forCurrentLocation uses the device current position. MKPlacemark wraps a CLPlacemark for MapKit compatibility. The calculate method is asynchronous and contacts Apple servers.

How to Draw the Route Polyline on the Map

Adding the overlay requires an MKMapViewDelegate renderer method:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    let renderer = MKPolylineRenderer(overlay: overlay)
    renderer.strokeColor = .blue
    renderer.lineWidth = 3.0
    return renderer
}

MKPolylineRenderer handles drawing the route path. Configure strokeColor, lineWidth, lineDashPattern, and alpha to match your app design. Without this renderer, the polyline exists but is invisible.

Zoom the map to fit the entire route:

let rect = route.polyline.boundingMapRect
self.mapView.setRegion(MKCoordinateRegion(rect), animated: true)

boundingMapRect returns the smallest rectangle enclosing the entire route. The map zooms and centers to display the full path.

How to Add Transport Type Switching with UISegmentedControl

Add a UISegmentedControl to the navigation bar with Car and Walking segments. Declare an outlet and a transport type variable:

@IBOutlet var segmentedControl: UISegmentedControl!
var currentTransportType = MKDirectionsTransportType.automobile

Hide the control initially and register for value changes:

segmentedControl.isHidden = true
segmentedControl.addTarget(self, action: #selector(showDirection), for: .valueChanged)

Update showDirection to use the selected transport type:

@IBAction func showDirection(sender: AnyObject) {
    switch segmentedControl.selectedSegmentIndex {
    case 0: currentTransportType = .automobile
    case 1: currentTransportType = .walking
    default: break
    }
    segmentedControl.isHidden = false
    
    guard let currentPlacemark = currentPlacemark else { return }
    
    let directionRequest = MKDirections.Request()
    directionRequest.source = MKMapItem.forCurrentLocation()
    directionRequest.destination = MKPlacemark(placemark: currentPlacemark)
    directionRequest.transportType = currentTransportType
    
    let directions = MKDirections(request: directionRequest)
    directions.calculate { routeResponse, routeError in
        guard let routeResponse = routeResponse else { return }
        
        self.mapView.removeOverlays(self.mapView.overlays)
        let route = routeResponse.routes[0]
        self.currentRoute = route
        self.mapView.addOverlay(route.polyline, level: .aboveRoads)
        
        let rect = route.polyline.boundingMapRect
        self.mapView.setRegion(MKCoordinateRegion(rect), animated: true)
    }
}

Remove old overlays before adding new ones. Change the route color based on transport type in the renderer:

renderer.strokeColor = (currentTransportType == .automobile) ? .systemBlue : .systemOrange

Blue for driving and orange for walking provides clear visual distinction.

How to Display Step-by-Step Directions

MKRoute contains a steps array of MKRoute.Step objects. Each step has instructions with human-readable text.

Create a RouteTableViewController:

class RouteTableViewController: UITableViewController {
    var routeSteps = [MKRoute.Step]()
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return routeSteps.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = routeSteps[indexPath.row].instructions
        cell.textLabel?.numberOfLines = 0
        return cell
    }
}

Store the current route in the map view controller:

var currentRoute: MKRoute?

Pass steps to the detail view controller through prepareForSegue:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showSteps" {
        let controller = segue.destination as! RouteTableViewController
        if let steps = currentRoute?.steps {
            controller.routeSteps = steps
        }
    }
}

Best Practices for MapKit Routing

Handle authorization denial gracefully. Show a message explaining why location access is needed with a button to open Settings. Cache route results to avoid repeated server calls for the same route. Show loading states with a spinner during route calculation. Validate that both source and destination coordinates are valid before requesting directions. Respect Apple rate limits for the directions API. Consider caching or throttling for production apps. Test with real devices because simulator location simulation does not reflect GPS accuracy or cellular data constraints.

Summary

This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.

Frequently Asked Questions

Can I test this on the iOS Simulator? Some features require a physical device with a compatible processor. The article notes specific sections where the Simulator is sufficient and where a real device is required.

What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.

All articles