MapKit | Drawing a Route on a Map | Swift + UIKit

Tianna Lewis
4 min readNov 15, 2021

A short series about working with iOS MapKit.

We are finally here, the last article in the series, and we're going to look at adding a route line between two points on a map. As a refresher, this mini-series focuses on 3 things, and we are currently covering topic 3 of 3. There were two previous articles that covered a bit of project setup and custom annotations, so if you want to see those first, check out the links below.

This mini-series focuses on 3 things:

  1. Adding a MapView Programmatically
  2. Adding Custom Annotations to a Map
  3. Drawing a Route on a Map [You are Here]

If you are more of an audio/visual person there are videos for each section, that are available on YouTube, and the video for this content is below.

These articles have gotten shorter and shorter and this is no different, the first thing we need to add to the project to accomplish our task of drawing a route is to define the route that will be drawn, which we’ll do in a new function we aptly name drawRoute. drawRoute will take one input an array of CLLocation objects when it is called, which is our routeCoordinates variable from before.

Draw a Route

func drawRoute(routeData: [CLLocation]) {
if routeCoordinates.count == 0 {
print("🟡 No Coordinates to draw")
return
}

let coordinates = routeCoordinates.map { (location) -> CLLocationCoordinate2D in
return location.coordinate
}

DispatchQueue.main.async {
self.routeOverlay = MKPolyline(coordinates: coordinates, count: coordinates.count)
self.mapView.addOverlay(self.routeOverlay!, level: .aboveRoads)
let customEdgePadding: UIEdgeInsets = UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 20)
self.mapView.setVisibleMapRect(self.routeOverlay!.boundingMapRect, edgePadding: customEdgePadding, animated: false)
}
}

Now that the function is done remember it needs to be called in viewDidLoad otherwise nothing will happen.

Define the Route Line

Now that we have our route defined to show up on the map, we need to create a line renderer and the mapView needs to know about it, so we will do this by adding another function to the MKMapVewDelegate extension at the bottom of the file. This time we are using the “rendererFor overlay…” function stub and updating. In my example, I chose to use a gradient renderer but you could also use a regular one and have the line be a single colour, but I just thought this looked nice.

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKGradientPolylineRenderer(overlay: overlay)
renderer.setColors([
UIColor(red: 0.02, green: 0.91, blue: 0.05, alpha: 1.00),
UIColor(red: 1.00, green: 0.48, blue: 0.00, alpha: 1.00),
UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.00)
], locations: [])
renderer.lineCap = .round
renderer.lineWidth = 3.0
return renderer
}

Conclusion

And with that, we have accomplished everything we set out to at the beginning. We added a map, added annotations, customized the annotations, and drew a route between them.

Seeing it all come together it looks like this.

//
// ViewController.swift
// Route
//
import UIKit
import MapKit
class ViewController: UIViewController {

var routeData : Route?
var routeCoordinates : [CLLocation] = []
var routeOverlay : MKOverlay?

let mapView : MKMapView = {
let map = MKMapView()
map.overrideUserInterfaceStyle = .dark
return map
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

mapView.delegate = self

setMapConstraints()

if let routeJSON = self.getJSON() {
parseJSON(jsonData: routeJSON)
}

addPins()

drawRoute(routeData: routeCoordinates)
}

func setMapConstraints() {
view.addSubview(mapView)

mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
mapView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}

func getJSON() -> Data? {
if let path = Bundle.main.path(forResource: "data", ofType: "json") {
do {
let data = try String(contentsOfFile: path).data(using: .utf8)
print("🟢 SUCCESS: JSON read successfully")
return data
} catch {
print("🛑 ERROR: Unable to read JSON")
}
}

return nil
}

func parseJSON(jsonData: Data) {
do {
routeData = try JSONDecoder().decode(Route.self, from: jsonData)

for feature in routeData?.features ?? [] {
let loc = CLLocation(
latitude: feature.geometry.coordinates[1],
longitude: feature.geometry.coordinates[0]
)
routeCoordinates.append(loc)
}
} catch {
print("🛑 ERROR: Unable to parse JSON")
}
}

func addPins() {
if routeCoordinates.count != 0 {
let startPin = MKPointAnnotation()
startPin.title = "start"
startPin.coordinate = CLLocationCoordinate2D(
latitude: routeCoordinates[0].coordinate.latitude,
longitude: routeCoordinates[0].coordinate.longitude
)
mapView.addAnnotation(startPin)

let endPin = MKPointAnnotation()
endPin.title = "end"
endPin.coordinate = CLLocationCoordinate2D(
latitude: routeCoordinates.last!.coordinate.latitude,
longitude: routeCoordinates.last!.coordinate.longitude
)
mapView.addAnnotation(endPin)
}
}

func drawRoute(routeData: [CLLocation]) {
if routeData.count == 0 {
print("🟡 No Coordinates to draw")
return
}

let coordinates = routeData.map { location -> CLLocationCoordinate2D in
return location.coordinate
}

DispatchQueue.main.async {
self.routeOverlay = MKPolyline(coordinates: coordinates, count: coordinates.count)
self.mapView.addOverlay(self.routeOverlay!, level: .aboveRoads)
let customEdgePadding : UIEdgeInsets = UIEdgeInsets(
top: 50,
left: 50,
bottom: 50,
right: 50
)
self.mapView.setVisibleMapRect(self.routeOverlay!.boundingMapRect, edgePadding: customEdgePadding,animated: true)
}
}
}extension ViewController : MKMapViewDelegate {

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}

var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "custom")

if annotationView == nil {
//CREATE VIEW
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "custom")
} else {
//ASSIGN ANNOTATION
annotationView?.annotation = annotation
}

//SET CUSTOM ANNOTATION IMAGES
switch annotation.title {
case "end":
annotationView?.image = UIImage(named: "pinEnd")
case "start":
annotationView?.image = UIImage(named: "pinStart")
default:
break
}

return annotationView
}

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKGradientPolylineRenderer(overlay: overlay)

renderer.setColors([
UIColor(red: 0.02, green: 0.91, blue: 0.05, alpha: 1.0),
UIColor(red: 1.0, green: 0.48, blue: 0.0, alpha: 1.0),
UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0),
], locations: [])
renderer.lineCap = .round
renderer.lineWidth = 3.0

return renderer
}

}

Links

YouTube Series
GitHub

--

--

Tianna Lewis

Learning and Building in the Open. Check out what I’ve been up to at tiannahenrylewis.com.