Frederick Squid Frederick Squid - 2 months ago 20
Swift Question

How to get a MKAnnotationView that's draggable without any delay?

The below code works and gives me a draggable annotation view. However, I've noticed that the annotation view is not draggable from the very beginning on, but rather only after the finger has rested for a short moment on the annotation view. When directly going into a drag movement the dragging doesn't affect the annotation view but instead pans the map. It certainly doesn't feel like drag'n'drop. Both on the device and in the simulator.

ViewController (Delegate of the MapView)

override func viewDidLoad() {
/// ...
let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(addPin))
mapView.addGestureRecognizer(gestureRecognizer)
}

func addPin(gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state != UIGestureRecognizerState.Began {
return
}
for annotation in mapView.annotations {
mapView.removeAnnotation(annotation)
}
let touchLocationInView = gestureRecognizer.locationInView(mapView)
let coordinate = mapView.convertPoint(touchLocationInView, toCoordinateFromView: mapView)
let annotation = DragAnnotation(coordinate: coordinate, title: "draggable", subtitle: "")
mapView.addAnnotation(annotation)
}

func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isKindOfClass(DragAnnotation) {
let reuseIdentifier = "DragAnnotationIdentifier"
var annotationView: MKAnnotationView!
if let dequeued = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseIdentifier) {
annotationView = dequeued
} else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
annotationView.annotation = annotation
annotationView.image = UIImage(named: "bluedisk2")
annotationView.canShowCallout = false
annotationView.draggable = true
return annotationView
}
return nil
}

func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, didChangeDragState newState: MKAnnotationViewDragState, fromOldState oldState: MKAnnotationViewDragState) {
switch (newState) {
case .Starting:
view.dragState = .Dragging
case .Ending, .Canceling:
view.dragState = .None
default: break
}
}


DragAnnotation

import UIKit
import MapKit

class DragAnnotation : NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D {
didSet {
print(coordinate)
}
}
var title: String?
var subtitle: String?

init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
}
}

Rob Rob
Answer

I don't think you can change the draggable delay, but you could disable it and add your own drag gesture that has no delay (or a shorter delay):

func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
    if annotation is MKUserLocation { return nil }

    var view = mapView.dequeueReusableAnnotationViewWithIdentifier("Foo")
    if view == nil {
        view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "Foo")
        view?.draggable = false

        let drag = UILongPressGestureRecognizer(target: self, action: #selector(handleDrag(_:)))
        drag.minimumPressDuration = 0 // set this to whatever you want
        drag.allowableMovement = .max
        view?.addGestureRecognizer(drag)
    } else {
        view?.annotation = annotation
    }

    return view
}

private var startLocation = CGPointZero

func handleDrag(gesture: UILongPressGestureRecognizer) {
    let location = gesture.locationInView(mapView)

    if gesture.state == .Began {
        startLocation = location
    } else if gesture.state == .Changed {
        gesture.view?.transform = CGAffineTransformMakeTranslation(location.x - startLocation.x, location.y - startLocation.y)
    } else if gesture.state == .Ended || gesture.state == .Cancelled {
        let annotationView = gesture.view as! MKAnnotationView
        let annotation = annotationView.annotation as! DragAnnotation

        let translate = CGPoint(x: location.x - startLocation.x, y: location.y - startLocation.y)
        let originalLocation = mapView.convertCoordinate(annotation.coordinate, toPointToView: mapView)
        let updatedLocation = CGPoint(x: originalLocation.x + translate.x, y: originalLocation.y + translate.y)

        annotationView.transform = CGAffineTransformIdentity
        annotation.coordinate = mapView.convertPoint(updatedLocation, toCoordinateFromView: mapView)
    }
}

By the way, if you're going to change the coordinate of an annotation, you will want to add dynamic keyword to your custom class coordinate declaration so that the appropriate KVO notifications will be issued.

class DragAnnotation: NSObject, MKAnnotation {
    dynamic var coordinate: CLLocationCoordinate2D
    var title: String?
    var subtitle: String?

    init(coordinate: CLLocationCoordinate2D, title: String? = nil, subtitle: String? = nil) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle
        super.init()
    }
}

Note, in this example, I set the delay to zero. That means that if you tap on the annotation (the typical UI for "show the callout"), this may prevent that from working correctly. It will treat this as a very short drag. This is why the standard UI requires a delay in the long press before dragging.

So, for this reason, I personally would hesitate to override this behavior shown above because it will be different then standard map behavior that the end user is familiar with. If your map behaves differently than any other maps on the user's device, this could be a source of frustration.