Building delightful, draggable iOS annotations for OpenTreeMap mobile

Building delightful, draggable iOS annotations for OpenTreeMap mobile

The iOS MapKit framework makes it easy to mark a point on a map, but making a draggable annotation that is a joy to use requires some extra work.

Azavea is building an iOS compliment to the OpenTreeMap web application to allow tree management organizations and the public to easily add to and correct the information for their city’s tree inventory. There is no better time to add information about a tree than when you are standing right next to the tree itself, iPhone in hand.

Adding new trees means placing new dots on the map. MapKit makes adding point annotations to a map easy. The MKPointAnnotation class is a basic implementation of the MKAnnotation protocol and provides the required coordinate property.

We cannot expect or require a user to accurately tap the exact location of a tree on their first attempt, so the application must give the user the ability to drag the annotation around on the map to fine tune the annotation’s position.

MapKit provides a default method of creating movable annotations. An object that implements the MKAnnotation protocol acts as a model, storing the coordinate of the point. A user working with an annotation on the map is actually seeing and interacting with a subclass of MKAnnotationView.

The rendering code for an MKMapView loops over the collection of annotations that have been added to the MKMapView and attempts to call the following method on the MKMapView delegate.

(MKAnnotationView *)mapView:(MKMapView *)mapView
          viewForAnnotation:(id <MKAnnotation>)annotation

If this delegate method is not implemented, a default instance of MKPinAnnotationView, a subclass of MKAnnotationView, is used to represent the annotation. MKAnnotationView instances define the appearance and behavior of an annotation within an MKMapView and offer a boolean draggable property.

Our first prototype of the “add a tree” feature for OpenTreeMap on iOS used this built-in ‘draggable’ functionality. The DraggableAnnotationViewDemo project shows a default, draggable annotation in action. If you install the DraggableAnnotationViewDemo on an iOS device and play with it for a while, you quickly uncover some problems with usability.

  1. The default pin annotations are difficult to “grab” with your finger. Even though the annotation has a buffer around it so that you don’t have to tap exactly on the pin, it still requires too much dexterity to get it just right every time.
  2. If you succeed in picking up a pin annotation, you discover that it is even more difficult to drop the pin in the correct place. Your finger is larger than the annotation itself, completely obscuring the target area on the map. When you do lift your finger, the pin “hops” onto the map some distance away from where you last touched the screen. In my testing I often gave up before I was able to position the pin exactly where I wanted.
  3. Dragging an annotation is a gesture, and gestures, because they are not visible, are not easily discoverable. A large portion of users will try to correct a mis-dropped pin by tapping a second time, rather than trying to drag.
  4. At distant zoom levels, dragging annotations is smooth, as you would expect. The further you zoom into the map, however, the more “jittery” the dragging becomes.

We started tackling these these usability problems by first designing a new graphic for the annotation.

There are several benefits to this annotation image, the first being the greatly increased size. Creating an annotation that spans 1/3 of the horizontal space on an iPhone screen results in a large target that is easy to grab. Having nearly a full finger width on either side of the center dot ensures that your finger does not obscure the drop target, allowing for precise and mistake free positioning. The increased size also gives us space to incorporate a “four direction” icon as a clue to discovering that the annotation is, in fact, draggable.

The CustomDraggableAnnotationDemo project improves on the DraggableAnnotationDemo by replacing the default MKPinAnnotationView with a subclass of MKAnnotationView incorporating the new annotation image. If you play with this demo on an iOS device you will see that it is an improvement, but it is not quite good enough.

You may notice that dragging the annotation, dropping it, and then immediately trying to drag it again does not often work. It seems that the MapKit implementation of the draggable property may have some kind of built-in delay, or perhaps there is just a bug in the touch handling. The draggable property also does nothing to eliminate the problem of “jittery” movement at high zoom levels.

As an aside, these demo projects illustrate how the MVC pattern makes it easy to create new versions of an interface without altering the core functionality of the application. The MKMapView and MKPointAnnotation are unchanged throughout all the demo projects. Any backend or controler code which adds an instance of MKPointAnnotation to the MKMapView will work with any of these annotation interfaces. Separating concerns makes it easy to itterate and improve one part of the application without breaking another.

The solution to the remaining usability problems is to leave the draggable property set to NO, and replace the functionality with custom touch event handlers and to manualy update to the annotation’s position within the MKMapView. Apple provides sample code for this in the “Legacy Map Techniques” section of the iOS developer documentation. This code is a good foundation, but it still produces “jittery” movement at high zoom levels.

The FinalCustomDraggableAnnotationDemo project eliminates the “jitter” by updating the CLLocationCoordinate2D of the underlying MKAnnotation rather than the center property of the MKAnnotationView. This project also includes a pair of UIGestureRecognizer instances added to the MKMapView which allow for handling single-taps on the map without interfering with double-tap-to-zoom. The single-tap gesture is used to move the annotation to a new point, allowing the user to reposition the annotation even if they do not know that the annotation is draggable.

While the FinalCustomDraggableAnnotationDemo project shows a big improvement over the MapKit default annotation behavior, it is not perfect. There is inevitable tension when attemping to drag a moveable object across a movable surface. The gesture handling in UIKit manages this tension using delays. A quick swipe will always move the map. Moving the annotation requires that the user touch the annotation and pause briefly before the annotation becomes the “target” of the drag.

The final solution to creating a quality user exerience around dragging map annotation required a lot more code than using the default MapKit behavior. I agree that the best code is no code at all but, in this case, the maintenance cost of the feature is more than paid for by frustration-free users who may contribute more to OpenTreeMap than they would otherwise.