Search code examples
iosswiftannotationsmapboxdrag

iOS Mapbox Updating the map while dragging annotation reverts the annotation to original coordinates


I'm trying to update some map components while dragging an annotations like highlighting a specific MGLPolygon and panning the map if the annotation is already dragged near the edge. I will use the later for this problem.

I tried the code https://docs.mapbox.com/ios/maps/examples/draggable-views/ and added some lines. Here's the exact copy with my changes.

import Mapbox

// Example view controller
class ViewController: UIViewController, MGLMapViewDelegate {

  var mapView: MGLMapView!

  override func viewDidLoad() {
    super.viewDidLoad()

    mapView = MGLMapView(frame: view.bounds)
    mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    mapView.styleURL = MGLStyle.streetsStyleURL
    mapView.tintColor = .darkGray
    mapView.zoomLevel = 1
    mapView.delegate = self
    view.addSubview(mapView)

    // Specify coordinates for our annotations.
    let coordinates = [
      CLLocationCoordinate2D(latitude: 0, longitude: -70),
      CLLocationCoordinate2D(latitude: 0, longitude: -35),
      CLLocationCoordinate2D(latitude: 0, longitude: 0),
      CLLocationCoordinate2D(latitude: 0, longitude: 35),
      CLLocationCoordinate2D(latitude: 0, longitude: 70)
    ]

    // Fill an array with point annotations and add it to the map.
    var pointAnnotations = [MGLPointAnnotation]()
    for coordinate in coordinates {
      let point = MGLPointAnnotation()
      point.coordinate = coordinate
      point.title = "To drag this annotation, first tap and hold."
      pointAnnotations.append(point)
    }

    mapView.addAnnotations(pointAnnotations)
  }

  // MARK: - MGLMapViewDelegate methods

  // This delegate method is where you tell the map to load a view for a specific annotation. To load a static MGLAnnotationImage, you would use `-mapView:imageForAnnotation:`.
  func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
    // This example is only concerned with point annotations.
    guard annotation is MGLPointAnnotation else {
      return nil
    }

    // For better performance, always try to reuse existing annotations. To use multiple different annotation views, change the reuse identifier for each.
    if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "draggablePoint") {
      return annotationView
    } else {
      let dav = DraggableAnnotationView(reuseIdentifier: "draggablePoint", size: 50)
      dav.mapView = mapView
      return dav
    }
  }

  func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
    return true
  }
}

// MGLAnnotationView subclass
class DraggableAnnotationView: MGLAnnotationView {
  var mapView: MGLMapView!
  var screen: CGRect!
  var mapBounds: CGRect!

  init(reuseIdentifier: String, size: CGFloat) {
    super.init(reuseIdentifier: reuseIdentifier)

    // `isDraggable` is a property of MGLAnnotationView, disabled by default.
    isDraggable = true

    // This property prevents the annotation from changing size when the map is tilted.
    scalesWithViewingDistance = false

    // Begin setting up the view.
    frame = CGRect(x: 0, y: 0, width: size, height: size)

    backgroundColor = .darkGray

    // Use CALayer’s corner radius to turn this view into a circle.
    layer.cornerRadius = size / 2
    layer.borderWidth = 1
    layer.borderColor = UIColor.white.cgColor
    layer.shadowColor = UIColor.black.cgColor
    layer.shadowOpacity = 0.1

    screen = UIScreen.main.bounds
    mapBounds = CGRect(
      x: screen.origin.x + 20,
      y: screen.origin.y + 20,
      width: screen.size.width - 40,
      height: screen.size.height - 40)
  }

  // These two initializers are forced upon us by Swift.
  override init(frame: CGRect) {
    super.init(frame: frame)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // Custom handler for changes in the annotation’s drag state.
  override func setDragState(_ dragState: MGLAnnotationViewDragState, animated: Bool) {
    super.setDragState(dragState, animated: animated)

    switch dragState {
    case .starting:
      print("Starting", terminator: "")
      startDragging()
    case .dragging:
      let pointCoordinate = self.mapView.convert(center, toCoordinateFrom: nil)

      if mapBounds.contains(center) {
        DispatchQueue.main.async {
          self.mapView.setCenter(pointCoordinate, animated: true)
        }
      }

      print(".", terminator: "")
    case .ending, .canceling:
      print("Ending")
      endDragging()
    case .none:
      break
    @unknown default:
      fatalError("Unknown drag state")
    }
  }

  // When the user interacts with an annotation, animate opacity and scale changes.
  func startDragging() {
    UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: [], animations: {
      self.layer.opacity = 0.8
      self.transform = CGAffineTransform.identity.scaledBy(x: 1.5, y: 1.5)
    }, completion: nil)

    // Initialize haptic feedback generator and give the user a light thud.
    if #available(iOS 10.0, *) {
      let hapticFeedback = UIImpactFeedbackGenerator(style: .light)
      hapticFeedback.impactOccurred()
    }
  }

  func endDragging() {
    transform = CGAffineTransform.identity.scaledBy(x: 1.5, y: 1.5)
    UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: [], animations: {
      self.layer.opacity = 1
      self.transform = CGAffineTransform.identity.scaledBy(x: 1, y: 1)
    }, completion: nil)

    // Give the user more haptic feedback when they drop the annotation.
    if #available(iOS 10.0, *) {
      let hapticFeedback = UIImpactFeedbackGenerator(style: .light)
      hapticFeedback.impactOccurred()
    }
  }
}

Everytime the self.mapView.setCenter(pointCoordinate, animated: true) gets called, the annotations goes back and forth to its original position.


Solution

  • Here is the code explaining the Adonis's solution. Essentially add a pan gesture to a custom annotation's view and update the coords as and when the annotation is panned.

    class CustomDraggableAnnotaionView: MGLAnnotationView {
    
    
    required init(
        reuseIdentifier: String?,
        image: UIImage?,
        annotation: CustomMapGLAnnotaion
    ) {
        super.init(reuseIdentifier: reuseIdentifier)
    
        setupDraggableAnnotations()
        self.layer.zPosition = 10
    }
    
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Draggable annotation handlers
    
    private func setupDraggableAnnotations() {
        addDraggableAnnotationGestureRecognizers()
    }
    
    private func addDraggableAnnotationGestureRecognizers() {
        let panGesture = UIPanGestureRecognizer(
            target: self,
            action: #selector(self.draggedView(_:))
        )
        let tapGesture = UITapGestureRecognizer(
            target: self,
            action: #selector(self.tappedAnnotation(_:))
        )
        self.isUserInteractionEnabled = true
        self.addGestureRecognizer(panGesture)
        self.addGestureRecognizer(tapGesture)
        for recognizer in self.gestureRecognizers! where recognizer is UITapGestureRecognizer {
            tapGesture.require(toFail: recognizer)
        }
        for recognizer in self.gestureRecognizers! where recognizer is UIPanGestureRecognizer {
            panGesture.require(toFail: recognizer)
        }
    }
    
    @objc func draggedView(_ sender: UIPanGestureRecognizer) {
        annotationObject?.draggable!.isCurrentlyDragging = true
        let point = sender.location(in: MapManager.shared.mapView)
        let coordinates = MapManager.shared.mapView.convert(
            point,
            toCoordinateFrom: MapManager.shared.mapView
        )
    
        annotationObject?.coordinate = coordinates
    
        if sender.state == .ended {
            // endDragging()
        } else if sender.state == .began {
            // startDragging()
            annotationObject?.draggable!.handler.didStartDragging()
        } else {
            // 
        }
    }
    

    }