SwiftUI Map Display Annotations

Viewed 4055

I decided to switch from wrapping MKMapView into a UIViewRepresentable to the new Map() in SwiftUI.

I was able to display correctly a MKMapRect into the Map() but I am unable to display two MKPointAnnotation there. Also my route between these annotation is not showing

It is requiring me to provide an RandomAccessCollection and a (Identifiable) -> MapAnnotationProtocol> but I do not know what to put there.

Any idea what should I put in (Identifiable) -> MapAnnotationProtocol ?

import SwiftUI
import MapKit

extension MKPointAnnotation: Identifiable { }

struct ItemView: View {
    @ObservedObject var itemViewModel: ItemViewModel
    @State var coll = [MKPointAnnotation]()
    
    func onAppear() {
        let requestAnnotation = MKPointAnnotation()
        requestAnnotation.coordinate = CLLocationCoordinate2D(latitude: 46.2004781, longitude: 6.1346497)
        requestAnnotation.title = "Package"
        
        let destinationAnnotation = MKPointAnnotation()
        destinationAnnotation.coordinate = CLLocationCoordinate2D(latitude: 47.1420446, longitude: 9.5204032)
        destinationAnnotation.title = "Destination"
        
        coll.append(requestAnnotation)
        coll.append(destinationAnnotation)
    }
    
    var body: some View {
        VStack {
            if let mapRect = itemViewModel.route?.polyline.boundingMapRect {
                Map(mapRect: .constant(mapRect), annotationItems: coll) { point in
                  MapAnnotation(coordinate: point.coordinate) {
                      Text(point.title ?? "")
                  }
                }
            }
        }
        .onAppear(perform: self.onAppear)
    }
    
}
4 Answers

You don't need to do MKPointAnnotation anymore. Currently, in Xcode 12.0 Beta, there are three structs that SwiftUI provides to satisfy the MapAnnotationProtocol:

  • MapAnnotation
  • MapMarker
  • MapPin

The RandomAccessCollection should be any collection (an array will do) of objects that adopt Identifiable. These used as the argument of the block in the final argument of the Map initializer.

Map(coordinateRegion: $container.region, 
    annotationItems: container.annotationLocations) { 
        (location) -> MapPin in
            MapPin(coordinate: location.coordinate)
    })

It seems to me that MapAnnotation requires you to implement the whole view that will be rendered on the map, so if you want both the red pin and some text, you will have to create that pin yourself (e.g. as an instance of Image with SF Symbol). Of course it doesn't look as good as the pin from MapMarker but at least it's a start.

Map(coordinateRegion: .constant(mapRect), annotationItems: coll) { annotationItem in
  MapAnnotation(coordinate: annotationItem.coordinate) {
    VStack {
      Group {
        Image(systemName: "mappin.circle.fill")
          .resizable()
          .frame(width: 30.0, height: 30.0)
        Circle()
          .frame(width: 8.0, height: 8.0)
      }
      .foregroundColor(.red)
      Text(annotationItem.title)
    }
  }
}

It should be like the following (not tested due to absent dependencies in provided snapshot)

Map(mapRect: .constant(mapRect), annotationItems: coll) { point in
   MapAnnotation(coordinate: point.coordinate) {
    Text(point.title ?? "")
   }
}

For anyone who searching for SwiftUI MKPolyline route

    struct MapView: View {
        
        @ObservedObject var viewModel: MapViewModel
        
        var body: some View {
            VStack(spacing: 0) {
                Text("Status")
                    .padding()
                MapViewRepresentable(exampleRoute: viewModel.exampleRoute)
                Button("Button", action: { })
                    .padding()
            }
        }
        
    }
    
    
    struct MapView_Previews: PreviewProvider {
        static var previews: some View {
            MapView(viewModel: MapViewModel())
        }
    }
    
    
    struct MapViewRepresentable: UIViewRepresentable {
        
        private let initialRegion = CoordinateRegion.initial
        
        let exampleRoute: MKPolyline
        
        func makeCoordinator() -> Coordinator {
            return Coordinator(self)
        }
        
        func makeUIView(context: UIViewRepresentableContext<MapViewRepresentable>) -> MKMapView {
            let mapView = MKMapView()
            mapView.delegate = context.coordinator
            mapView.setRegion(initialRegion, animated: false)
            mapView.showsUserLocation = true
            mapView.addOverlay(exampleRoute)
            return mapView
        }
    
        func updateUIView(_ view: MKMapView,
                          context: UIViewRepresentableContext<MapViewRepresentable>) {
    
        }
        
        class Coordinator: NSObject, MKMapViewDelegate {
            let parent: MapViewRepresentable
            init(_ mapView: MapViewRepresentable) {
                parent = mapView
            }
            func mapView(_ mapView: MKMapView,
                         rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
                let polylineRenderer: MKPolylineRenderer = MKPolylineRenderer(overlay: overlay)
                polylineRenderer.lineWidth = 3
                polylineRenderer.strokeColor = UIColor.red
                return polylineRenderer
            }
        }
        
    }


class MapViewModel: ObservableObject {
    
    @Published private(set) var exampleRoute = createPolylines()
    
    static func createPolylines() -> MKPolyline {
        var coordinates1 = createExampleRoute()
        return MKPolyline(coordinates: &coordinates1, count: coordinates1.count)
    }
    
    static func createExampleRoute() -> [CLLocationCoordinate2D] {
        return [CLLocationCoordinate2D(latitude: 60.608905, longitude: 39.814428),
                CLLocationCoordinate2D(latitude: 60.609073, longitude: 39.812000),
                CLLocationCoordinate2D(latitude: 60.610429, longitude: 39.812071)]
    }
    
}


struct CoordinateRegion {
    static let initial = MKCoordinateRegion(center: CLLocationManager().location?.coordinate
                                                    ?? CLLocationCoordinate2D(latitude: 60.608905,
                                                                              longitude: 39.814428),
                                            span: MKCoordinateSpan(latitudeDelta: 0.05,
                                                                   longitudeDelta: 0.05))
    func getCurrentLocation() -> CLLocationCoordinate2D {
        return CLLocationManager().location!.coordinate
    }
    
}
Related