4

I try to create Map view with annotations and overlays using only iOS 14 SwiftUI Map. I managed to create annotations but I am not able to create visible overlays because of lack of MKOverlayRenderer from MKMapView delegate. How can I attach to Map view delegate so it can run its method

    Map(coordinateRegion: $region, showsUserLocation: true, userTrackingMode: $userTrackingMode, annotationItems: annotations)) { annotation in
        MapAnnotation(coordinate: CLLocationCoordinate2DMake(annotation.latitude, annotation.longitude),
                      anchorPoint: CGPoint(x: 0.5, y: 0.7)) {
            NavigationLink(destination: DetailsView()) {
                VStack {
                    Image(systemName: "mappin")
                        .font(.title)
                        .foregroundColor(.red)
                    Text(annotation.name.split(separator: " ").joined(separator: "\n"))
                        .font(.caption)
                        .bold()
                        .multilineTextAlignment(.center)
                        .foregroundColor(.black)
                        .padding(2)
                        .background(RoundedRectangle(cornerRadius: 4).fill(Color.white.opacity(0.8)))
                        .layoutPriority(1)
                }
            }
        }
    }
    .addOverlays(mapOverlays, delegate: mapDelegate)

and Map extension method:

extension Map {
    func addOverlays(_ overlays: [MKOverlay], delegate: MKMapViewDelegate) -> some View {
        // MKMapView.appearance().delegate = delegate // THIS DOES NOT WORK AT ALL

        MKMapView.appearance().addOverlays(overlays)

        return self
    }
}

and delegate:

class MapDelegate: NSObject, MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if overlay is MKCircle {
            let circle = MKCircleRenderer(overlay: overlay)
            circle.strokeColor = .systemRed
            circle.fillColor = .systemRed
            circle.alpha = 0.2
            circle.lineWidth = 1

            return circle
        }
        return MKOverlayRenderer()
    }
}
Paweł Madej
  • 1,229
  • 23
  • 42

2 Answers2

5

Definitely not possible, SwiftUI Map does not yet support overlays. Using UIViewRepresentsable is easy, see SO example here:
Swiftui how to use MKOverlayRenderer? using the following code:

struct MapView: UIViewRepresentable {
    @Binding var route: MKPolyline?
    let mapViewDelegate = MapViewDelegate()

    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame: .zero)
    }

    func updateUIView(_ view: MKMapView, context: Context) {
        view.delegate = mapViewDelegate                          // (1) This should be set in makeUIView, but it is getting reset to `nil`
        view.translatesAutoresizingMaskIntoConstraints = false   // (2) In the absence of this, we get constraints error on rotation; and again, it seems one should do this in makeUIView, but has to be here
        addRoute(to: view)
    }
}

private extension MapView {
    func addRoute(to view: MKMapView) {
        if !view.overlays.isEmpty {
            view.removeOverlays(view.overlays)
        }

        guard let route = route else { return }
        let mapRect = route.boundingMapRect
        view.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), animated: true)
        view.addOverlay(route)
    }
}

class MapViewDelegate: NSObject, MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        let renderer = MKPolylineRenderer(overlay: overlay)
        renderer.fillColor = UIColor.red.withAlphaComponent(0.5)
        renderer.strokeColor = UIColor.red.withAlphaComponent(0.8)
        return renderer
    }
}
dasdom
  • 13,975
  • 2
  • 47
  • 58
theExpert
  • 51
  • 1
  • 5
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. – Procrastinator Sep 07 '21 at 06:23
  • 1
    Thanks, added the code component found at the given link, this is a ready to use UIViewRepresentable MKMapView wrapper that allows to use overlays as well, this is not yet supported by SwiftUI MAP up to and including IOS15 – theExpert Sep 08 '21 at 07:23
4

Probably this is a pretty late answer, but could be useful to someone else.

After trying a few approaches, I came to this solution (btw, I would say that's not even close to a fancy code... but it works).

First, you have to create a class that implements the MKMapViewDelegate (in my case I needed to show an overlay with a custom theme on the Map, but you can use it to show a Polyline or a Polygon as well).

class MapCustomDelegate: NSObject, MKMapViewDelegate {
    var parent: MKMapView
    
    init(_ parent: MKMapView) {
        self.parent = parent
    }
    
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
      if let tileOverlay = overlay as? MKTileOverlay {
        let renderer = MKTileOverlayRenderer(overlay: tileOverlay)
        return renderer
      }
      return MKOverlayRenderer()
    }
}

Then in the view you have to implement the delegate using the MKMapView.appearance() instance, like this:

struct MapCustomView: View {
    // Properties
    @StateObject private var viewModel: MapViewModel = MapViewModel()
    private let mapAppearanceInstance = MKMapView.appearance()
    private var mapCustomDelegate: MapCustomDelegate = MapCustomDelegate(MKMapView.appearance())                     
    ..
                    
    var body: some View {
       VStack {   
          Map(coordinateRegion: $viewModel.mapRegion,
              annotationItems: viewModel.items) { item in
                    
              MapAnnotation(coordinate: item.location) {
                ..
              }
          }
          .onAppear {
             // viewModel.mapOverlay -> MKTileOverlay
             self.mapAppearanceInstance.delegate = mapCustomDelegate                         
             self.mapAppearanceInstance.addOverlay(viewModel.mapOverlay)
          }
     }
}

And it should work. Hope this helps :)

Timmy
  • 4,098
  • 2
  • 14
  • 34
dredg87
  • 51
  • 2