Search code examples
iosmapkitdragmkannotationview

Is it possible to have a point annotation view that can be dragged OR selected?


I have a MKMapView that shows some MKPinAnnotationView objects.
I want to be able to drag an annotation view, but I want also to be able to select it.

The problem:

When I implement the delegate function

func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {…}  

this function is immediately called when I touch down to the annotation view. It shows an alert, and thus prevents the annotation view from dragging.

When I do not implement the delegate function, I can drag the annotation view as expected.

I think the following should be possible:
- Touch down to the annotation view.
- When I drag, move the annotation view.
- Else, i.e. when I touch up from the annotation view, select it.

How can I achieve this?

EDIT:

My pin annotation view does not have a callout. Rather, when it is selected, it shows an UIAlertController so that the user can choose further actions. If so, the mapView is dimmed an is not accessible.

The behaviour that I want to implement is:

If i touch the pin annotation view (finger down), nothing should happen.

If I then move the finger (still down), the pin annotation view should be dragged. If I then lift the finger, the pin annotation view will not be selected.

If, however, I do not mode the finger, but simply lift it, The pin annotation view will be selected (and the alert view should be shown).

I hope this clarifies the situation.


Solution

  • One possible solution is to handle the dragging yourself and utilize a long press gesture to manage the actions you desire (such as when to display an alert).

    This solution is built around Rob's detailed answer here and adds some additional logic to handle the specifics of your question (namely the wasMoved instance property).

    private var startLocation = CGPoint(x: 0.0, y: 0.0)
    private var wasMoved = false
    
    func handleLongPress(_ sender: UILongPressGestureRecognizer) {
        let location = sender.location(in: mapView)
    
        switch sender.state {
        case .began:
            startLocation = location
        case .changed:
            wasMoved = true
            sender.view?.transform = CGAffineTransform(translationX: location.x - startLocation.x, y: location.y - startLocation.y)
        case .ended, .cancelled:
            if wasMoved {
                let annotationView = sender.view as! MKAnnotationView
                let annotation = annotationView.annotation as! MKPointAnnotation
    
                let translate = CGPoint(x: location.x - startLocation.x, y: location.y - startLocation.y)
                let originalLocation = mapView.convert(annotation.coordinate, toPointTo: mapView)
                let updatedLocation = CGPoint(x: originalLocation.x + translate.x, y: originalLocation.y + translate.y)
    
                annotationView.transform = CGAffineTransform.identity
                annotation.coordinate = mapView.convert(updatedLocation, toCoordinateFrom: mapView)
            } else {
                let alert = UIAlertController(title: "Alert", message: "Here is my alert!", preferredStyle: .alert)
    
                let ok = UIAlertAction(title: "OK", style: .default, handler: nil)
                alert.addAction(ok)
    
                present(alert, animated: true, completion: nil)
            }
            wasMoved = false
        default:
            break
        }
    }
    

    And your mapView(_:viewFor:) delegate method would look like:

    extension ViewController: MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            guard let reusablePin = mapView.dequeueReusableAnnotationView(withIdentifier: "Pin") as? MKPinAnnotationView else {
                let pin = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "Pin")
    
                // Add the long press gesture recognizer to the annotation view
                let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
                longPress.minimumPressDuration = 0
                pin.addGestureRecognizer(longPress)
    
                return pin
            }
    
            reusablePin.annotation = annotation
            return reusablePin
        }
    }