简体   繁体   中英

SwiftUI Map Overlays without UIViewRepresentable

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()
    }
}

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
    }
}

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:)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM