Subclassing MKAnnotationView and overriding setDragState

Solution 1:

I had the pin drag working but was trying to figure out why the pin annimations that occur when you don't override setDragState - no longer work in my implementation. Your question contained my answer .. Thanks!

Part of the problem with your code is that once you override the setDragState function, per the xcode documentation, you are responsible for updating the dragState variable based on the new state coming in. I would also be a little concerned about your code calling itself (setDragState calling [self setDragState]).

Here is the code I ended up (with your help) that does all of the lifts, drags and drops as I expect them to occur. Hope this helps you too!

- (void)setDragState:(MKAnnotationViewDragState)newDragState animated:(BOOL)animated
{
    if (newDragState == MKAnnotationViewDragStateStarting)
    {
        // lift the pin and set the state to dragging

        CGPoint endPoint = CGPointMake(self.center.x,self.center.y-20);
        [UIView animateWithDuration:0.2
                         animations:^{ self.center = endPoint; }
                         completion:^(BOOL finished)
                             { self.dragState = MKAnnotationViewDragStateDragging; }];
    }
    else if (newDragState == MKAnnotationViewDragStateEnding)
    {
        // save the new location, drop the pin, and set state to none

        /* my app specific code to save the new position
        objectObservations[ACTIVE].latitude = pinAnnotation.coordinate.latitude;
        objectObservations[ACTIVE].longitude = pinAnnotation.coordinate.longitude;
        posChanged = TRUE;
        */

        CGPoint endPoint = CGPointMake(self.center.x,self.center.y+20);
        [UIView animateWithDuration:0.2
                         animations:^{ self.center = endPoint; }
                         completion:^(BOOL finished)
                             { self.dragState = MKAnnotationViewDragStateNone; }];
    }
    else if (newDragState == MKAnnotationViewDragStateCanceling)
    {
        // drop the pin and set the state to none

        CGPoint endPoint = CGPointMake(self.center.x,self.center.y+20);
        [UIView animateWithDuration:0.2
                         animations:^{ self.center = endPoint; }
                         completion:^(BOOL finished)
                             { self.dragState = MKAnnotationViewDragStateNone; }];
    }
}

Solution 2:

While Brian's solution worked, it lacked taking into account the users finger blocking the annotation view which is being manipulated.

This means that the user could not precisely place the pin once he was dragging it. The standard MKPinAnnotationView does a great job at this, what happens is when the finger begins dragging, the pin is lifted above the finger, and the visual point of the pin is used for placement not the previous centre point which now resides under the finger.

In addition to this my implementation also adds another animation when dropping the pin after dragging, by lifting the pin and dropping it with a higher speed. This is very close the the native user experience and will be apreciated by your users.

Please check out my gist on GitHub for the code.

What's really cool is setting a delegate is optional, optionally a notification is sent when the annotation view is dropped back onto the map.

Solution 3:

I didn't study Ben's code much but it didn't worked for me. So I tried Brian's and it works great. Thanks a lot! I've been trying to solve annotation's animation during drag'n'drop for a long time.

But I have one suggestion to Brian's solution. I think that it would be better to support MKMapKit's delegate and notify it about changing dragState and save new position within standard delegate's method: - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)annotationView didChangeDragState:(MKAnnotationViewDragState)newState fromOldState:(MKAnnotationViewDragState)oldState. Here's my code:

DraggableAnnotationView.h:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface DraggableAnnotationView : MKAnnotationView 
{
    id <MKMapViewDelegate> delegate;
    MKAnnotationViewDragState dragState;
}

@property (nonatomic, assign) id <MKMapViewDelegate> delegate;
@property (nonatomic, assign) MKAnnotationViewDragState dragState;
@property (nonatomic, assign) MKMapView *mapView;

@end

DraggableAnnotationView.m:

#import "DraggableAnnotationView.h"
#import "MapAnnotation.h"

@implementation DraggableAnnotationView
@synthesize delegate, dragState, mapView;



- (void)setDragState:(MKAnnotationViewDragState)newDragState animated:(BOOL)animated
{
    [delegate mapView:mapView annotationView:self didChangeDragState:newDragState fromOldState:dragState];

    if (newDragState == MKAnnotationViewDragStateStarting) {
        // lift the pin and set the state to dragging
        CGPoint endPoint = CGPointMake(self.center.x,self.center.y-20);
        [UIView animateWithDuration:0.2
                         animations:^{ self.center = endPoint; }
                         completion:^(BOOL finished)
         { dragState = MKAnnotationViewDragStateDragging; }];
    } else if (newDragState == MKAnnotationViewDragStateEnding) {
        // drop the pin, and set state to none

        CGPoint endPoint = CGPointMake(self.center.x,self.center.y+20);
        [UIView animateWithDuration:0.2
                         animations:^{ self.center = endPoint; }
                         completion:^(BOOL finished)
         { dragState = MKAnnotationViewDragStateNone; }];
    } else if (newDragState == MKAnnotationViewDragStateCanceling) {
        // drop the pin and set the state to none

        CGPoint endPoint = CGPointMake(self.center.x,self.center.y+20);
        [UIView animateWithDuration:0.2
                         animations:^{ self.center = endPoint; }
                         completion:^(BOOL finished)
         { dragState = MKAnnotationViewDragStateNone; }];
    }
}

- (void)dealloc 
{
    [super dealloc];
}

@end