Bug
Bug

Reputation: 2562

Adding moving circle overly on MKMapView on iOS 16 and above

I want to add a moving circle on MKMapView it will remain in the centre of the map and start moving when the user moves the map and zoom in and zoom out when the user zooms in/out the map I am able to achieve this by using the centre coordinate of the map but when I move the app it is flickering as I am removing and adding the MKCircle overlay when the coordinate changes

What I want to achieve:- A moving circle/Annotation on top of the mapview which moves/zoom along with mapview

Here is the code

Adding pan gesture and circle overlay here

func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        mapView.isUserInteractionEnabled = true
        mapView.showsUserLocation = true
        mapView.showsCompass = false

let panGesture =  UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.addPinBasedOnGesture(_:)))
       panGesture.delegate = context.coordinator
      mapView.addGestureRecognizer(panGesture)
        
        return mapView
}

Adding and removing method which updated the moving circle coordinate to

func updateMovingCircle(regionToUpdate: MKCoordinateRegion, view: MKMapView) {
        DispatchQueue.main.async {
            addMovingCircle(to: view, movingLocation: CLLocationCoordinate2D(latitude: regionToUpdate.center.latitude, longitude: regionToUpdate.center.longitude), radius: radius)
        }
    }

func to add and remove circle

func addMovingCircle(to view: MKMapView, movingLocation: CLLocationCoordinate2D, radius: Int) {
        for overlay in overlays where overlay.title == "1" {
            view.removeOverlay(overlay)
        }
        let radius: Double = Double(radius) * 1.5
        let movingCircle = MKCircle(center: movingLocation, radius: radius * 1000)
        movingCircle.title = "1"
        view.addOverlay(movingCircle)
    }

gesture recognizer delegate method on coordinator

 @objc func addPinBasedOnGesture(_ gestureRecognizer: UIGestureRecognizer) {
        if let mapView = gestureRecognizer.view as? MKMapView {
          mapViewController.updateMovingCircle(regionToUpdate: mapView.region, view: mapView)
        }
    }
    
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        return true
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

Overlay renderer

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {        
        
        if let circleOverlay = overlay as? MKCircle {
            let circleRenderer = MKCircleRenderer(overlay: circleOverlay)
            circleRenderer.strokeColor = UIColor(Color.SG.purple)
            circleRenderer.lineWidth = 2
            circleRenderer.lineDashPattern = [2, 5]
            return circleRenderer
        }
        
        return MKOverlayRenderer()
    }

This code is working but the experience is not smooth and circle is flickering

Upvotes: 0

Views: 112

Answers (1)

sonle
sonle

Reputation: 8866

I would like to go with a different approach. You can add the entire MapView inside a ZStack and a MKCircle above the map. Then, no matter how you interact with the map, MKCircle will stay at the center.

var body: some View {
    @State private var zoomFactor: Double = 10

    ZStack {
        MapViewSUI(zoomFactor: $zoomFactor)

        Image(systemName: "mappin.circle")
            .resizable()
            .frame(width: 64 * (zoomFactor / 10), height: 64 * (zoomFactor / 10))
            .foregroundColor(.red)
    }
}

Updated: Here is the idea for zoom action

  • Declare a @State property to pass in MapViewSUI which presents MKCircle size.
  • Whenever the map's region changes, calculate the zoom factor and assign it to this @State to update MKCircle size.
struct MapViewSUI: UIViewRepresentable {
    @Binding var zoomFactor: Double
    ...
    
    class Coordinator: NSObject, MKMapViewDelegate {
        var map: MapViewSUI
        ...
        //This is formula to calculate zoom level and it is not my code
        func getZoomLevel(from mapView: MKMapView) -> Double {
            log2(360 * ((mapView.frame.size.width/256.0) / mapView.region.span.longitudeDelta));
        }

        func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
            map.zoomFactor = getZoomLevel(from: mapView)
        }
    }
}

You may want to change the view size or add animation if needed. Output looks like this:

enter image description here

Upvotes: 0

Related Questions