(SwiftUI) How to (fix it functions of) draw a route in a MapKit view

We struggled to create our own iOS map app because drawing routes on top of MapKit in SwiftUI was extremely challenging.

Issue 1: Misuse of overlay

Issue Description

The overlay method in SwiftUI‘s Map component expects elements that conform to ShapeStyle, but an attempt was made to use it to render a MKPolyline overlay, causing the error: type '() -> ()' cannot conform to 'ShapeStyle'.

Problematic Code Example

Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true)
    .overlay {
        if let polyline = routePolyline {
            MapPolyline(polyline)
                .stroke(Color.blue, lineWidth: 5)
        }
    }

Solution

Since the overlay method in SwiftUI expects ShapeStyle elements, it is not suitable for displaying MKPolyline on a Map. To correctly display an MKPolyline, you need to integrate MKMapView into SwiftUI using UIViewRepresentable and use MKPolylineRenderer to render the polyline.

Correct Code Example

struct MapView: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion
    @Binding var routePolyline: MKPolyline?

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

        init(parent: MapView) {
            self.parent = parent
        }

        func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            if let polyline = overlay as? MKPolyline {
                let renderer = MKPolylineRenderer(polyline: polyline)
                renderer.strokeColor = .blue
                renderer.lineWidth = 5
                return renderer
            }
            return MKOverlayRenderer(overlay: overlay)
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        mapView.setRegion(region, animated: true)
        mapView.showsUserLocation = true
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)
        mapView.removeOverlays(mapView.overlays)
        if let polyline = routePolyline {
            mapView.addOverlay(polyline)
        }
    }
}

Issue 2: Misuse of annotationItems

Issue Description

MKPolyline is not an annotation (pin or marker), but rather a route (overlay) on the map. However, annotationItems is a property for annotation items, so trying to use MKPolyline here caused an error. Additionally, an error occurred because MKPolyline does not conform to Identifiable.

Problematic Code Example

Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, annotationItems: routePolyline != nil ? [routePolyline!] : []) { polyline in
    MapPolyline(polyline)
        .stroke(Color.blue, lineWidth: 5)
}

Solution

Instead of using annotationItems, which is intended for annotations, treat MKPolyline as an overlay on the map. This can be done by using UIViewRepresentable to wrap an MKMapView and add the polyline as an overlay.

Correct Code Example

struct ContentView: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )

    @State private var routePolyline: MKPolyline?

    var body: some View {
        VStack {
            MapView(region: $region, routePolyline: $routePolyline)
                .frame(height: 400)
                .edgesIgnoringSafeArea(.top)

            Button(action: {
                searchRoute()
            }) {
                Text("Search Route")
            }
            .padding()

            Spacer()
        }
    }

    func searchRoute() {
        let startCoordinate = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
        let destinationCoordinate = CLLocationCoordinate2D(latitude: 37.7849, longitude: -122.4294)

        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: startCoordinate))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destinationCoordinate))
        request.transportType = .automobile

        let directions = MKDirections(request: request)
        directions.calculate { response, error in
            if let error = error {
                print("Route search error: \(error.localizedDescription)")
                return
            }

            if let route = response?.routes.first {
                self.routePolyline = route.polyline
                self.region = MKCoordinateRegion(route.polyline.boundingMapRect)
            }
        }
    }
}

Issue 3: MKPolyline Does Not Conform to Identifiable

Issue Description

An error was encountered when trying to use annotationItems because MKPolyline does not conform to Identifiable. This is closely related to the issue of treating MKPolyline as an annotation, which is incorrect.

Solution

Avoid using MKPolyline as an annotation. Instead, handle it properly as an overlay using UIViewRepresentable to integrate MKMapView.

Conclusion

The SwiftUI Map component is suitable for basic map displays but lacks support for rendering custom overlays such as MKPolyline. The recommended approach is to integrate MKMapView into SwiftUI using UIViewRepresentable, which allows access to advanced MapKit features and ensures safe and flexible implementation of map functionality.

Full Sample Code

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion
    @Binding var routePolyline: MKPolyline?

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

        init(parent: MapView) {
            self.parent = parent
        }

        func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            if let polyline = overlay as? MKPolyline {
                let renderer = MKPolylineRenderer(polyline: polyline)
                renderer.strokeColor = .blue
                renderer.lineWidth = 5
                return renderer
            }
            return MKOverlayRenderer(overlay: overlay)
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        mapView.setRegion(region, animated: true)
        mapView.showsUserLocation = true
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)

        mapView.removeOverlays(mapView.overlays)

        if let polyline = routePolyline {
            mapView.addOverlay(polyline)
        }
    }
}

struct ContentView: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )
    
    @State private var routePolyline: MKPolyline?

    var body: some View {
        VStack {
            MapView(region: $region, routePolyline: $routePolyline)
                .frame(height: 400)
                .edgesIgnoringSafeArea(.top)

            Button(action: {
                searchRoute()
            }) {
                Text("Search")
            }
            .padding()

            Spacer()
        }
    }

    func searchRoute() {
        let startCoordinate = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
        let destinationCoordinate = CLLocationCoordinate2D(latitude: 37.7849, longitude: -122.4294)
        
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: startCoordinate))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destinationCoordinate))
        request.transportType = .automobile
        
        let directions = MKDirections(request: request)
        directions.calculate { response, error in
            if let error = error {
                print("Error: \(error.localizedDescription)")
                return
            }
            
            if let route = response?.routes.first {
                self.routePolyline = route.polyline
                self.region = MKCoordinateRegion(route.polyline.boundingMapRect)
            }
        }
    }
}

#Preview {
    ContentView()
}