9

This is about an iPhone App using MKMapKit:

I created a custom MKAnnotationView for a draggable Annotation. I want to create a custom animation. I set a custom pin image and the annotation is draggable (which both is not shown here, it happens in the mapview) with the following code:

- (void) movePinUpFinished {

     [super setDragState:MKAnnotationViewDragStateDragging];
     [self setDragState:MKAnnotationViewDragStateDragging];
}

- (void) setDragState:(MKAnnotationViewDragState) myState {
     if (myState == MKAnnotationViewDragStateStarting) {
          NSLog(@"starting");
          CGPoint endPoint = CGPointMake(self.center.x,self.center.y-20);
          self.center = endPoint;
          [self movePinUpFinished];
     }
     if (myState == MKAnnotationViewDragStateEnding) {
          NSLog(@"ending");
          [super setDragState:MKAnnotationViewDragStateEnding];
          [self setDragState:MKAnnotationViewDragStateNone];
          [super setDragState:MKAnnotationViewDragStateNone];
     }
     if (myState == MKAnnotationViewDragStateDragging) {
          NSLog(@"dragging");
     }
     if (myState == MKAnnotationViewDragStateCanceling) {
          NSLog(@"cancel");
     }
     if (myState == MKAnnotationViewDragStateNone) {
          NSLog(@"none");
     }
}

Everything works fine, the annotation is moved up a bit, is draggable and when i release the annotation, the mapview receives the "dragstateending".

But now I want the animation to run over a time period and change the dragStateStarting to the following:

if (myState == MKAnnotationViewDragStateStarting) {
          NSLog(@"starting");
          CGPoint endPoint = CGPointMake(self.center.x,self.center.y-20);
          [UIView animateWithDuration:1.0
           animations:^{ self.center = endPoint; }
           completion:^(BOOL finished){ [self movePinUpFinished]; }];
     }

The animations runs as wanted over the period of a second and the annotation is draggable. But when I release the annotation, the mapview is not receiving the ending through the delegat. What I also recognized was that when I am doing the animation with "UIView animateWithDuration..." is that immedently after beginning the dragging, as the animation starts, the ballon of the annotation opens. When i am setting the new center without the animation, the balloon keeps closed and is only opened after finishing the dragging by releasing the annotation.

What am I doing wrong? Is this the right way to override setDragState. Do I really have to call the super class? But without setting the dragstate in the superclass my mapview didnt realized the changes of the dragstate.

I wonder about the original implementation of MKPinAnnotationView, but because it is an internal Class I couldn't find a description of the setDragState method.

Thx for help. Cheers,

Ben

Ben Zwak
  • 225
  • 1
  • 3
  • 8

3 Answers3

23

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; }];
    }
}
William Denniss
  • 16,089
  • 7
  • 81
  • 124
Brian Johnson
  • 246
  • 3
  • 3
  • How did it work perfectly? When you drag the pin outside your screen, it results in a crash. – akshay1188 Jan 13 '12 at 08:24
  • This works, but one thing baffles me: In my MKMapViewDelegate, I override mapView:annotationView:didChangeDragState. MKAnnotationView base class produces two transitions: From 0->1 when the drag starts, and 1->4 when it's ending. This solution above seems to follow Apple's guidelines to the T, but it produces a 0->2 and a 2->0 transition. I can live with it but it's weird - I think Apple's documentation is off, to be honest. – n13 Mar 17 '12 at 08:38
  • 1
    I think you need to call `self.dragState = ...`, not `dragState =`. Otherwise, the KVO notifications won't get sent, and the map view won't see the dragging state change. – Rick Aug 03 '12 at 03:52
  • Good for animating picking up pin, but... the problem is placing the pin, the user cannot place it precisely because the bottom of the "pin" image will be 20 pixels higher, when dropped it goes back 20 pixels lower. – Daniel Sep 27 '12 at 18:59
  • @Rick, I agree. In fact, for me the code wouldn't compile in iOS 6 / XCode 4.5 without changing it to `self.dragState`. I have proposed an edit. – William Denniss Oct 27 '12 at 03:31
  • Did anyone find a solution for the problem described by @Daniel ? – Wirsing Jul 30 '13 at 13:56
  • I tried this exactly, and I only get the up animation. It refuses to drop at the end. I'm on iOS7. – Travis Griggs Feb 12 '14 at 19:33
  • re; Daniels comment. I compared to how the pin drop works in Apple's Maps app, and it works very similarly- the drop does not return the pin from whence it came if you have moved the pin at all. I guess we should copy the Maps implementation for consistency- although i think its odd, personally. – Peter Johnson Jun 17 '15 at 09:52
  • If you want to avoid a pin drop on placement, a usable alternative effect is to instead set the pin's alpha to transparent, then animate it back in, before setting dragState to MKAnnotationDragViewStateNone. – Peter Johnson Jun 17 '15 at 10:02
3

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.

Community
  • 1
  • 1
Daniel
  • 23,129
  • 12
  • 109
  • 154
2

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
JakubM
  • 2,685
  • 2
  • 23
  • 33