How do I animate MKAnnotationView drop?
Asked Answered
U

7

30

I have a custom MKAnnotationView where I set my image myself in viewForAnnotation. How do I animate it's drop like I can with MKPinAnnotationView?

My code is

- (MKAnnotationView *)mapView:(MKMapView *)map viewForAnnotation:(id <MKAnnotation>)annotation
{
    static NSString *AnnotationViewID = @"annotationViewID";

    MKAnnotationView *annotationView = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:AnnotationViewID];

    if (annotationView == nil)
    {
        annotationView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:AnnotationViewID] autorelease];
    }

    annotationView.image = [UIImage imageNamed:@"blah.png"];
    annotationView.annotation = annotation;

    return annotationView;
}
Unroot answered 24/7, 2011 at 18:45 Comment(1)
This is not a drop animation, but simply a nice animation https://mcmap.net/q/130843/-ios-mkmapview-animate-map-pinsWorcester
U
48

Implement the didAddAnnotationViews delegate method and do the animation yourself:

- (void)mapView:(MKMapView *)mapView 
          didAddAnnotationViews:(NSArray *)annotationViews
{
    for (MKAnnotationView *annView in annotationViews)
    {
        CGRect endFrame = annView.frame;
        annView.frame = CGRectOffset(endFrame, 0, -500);
        [UIView animateWithDuration:0.5 
                animations:^{ annView.frame = endFrame; }];
    }
}
Uninterested answered 24/7, 2011 at 20:23 Comment(3)
Thanks, that worked! How would I make it bounce instead of drop in from top?Unroot
You can use all the UIView animation techniques with the annotation view (it's a subclass of UIView). The above example just changes the y coordinate but you can do other transforms. Search SO for something like "uiview bounce animation" for examples. Here's one: https://mcmap.net/q/130844/-mimic-uialertview-bounce/…. You'll need to add QuartzCore framework, import it, and change "view" to "annView".Uninterested
@AnnaKarenina but how can we add CustomView as our pinView and customCallOut any references.Octopod
I
57

One problem with the code above by Anna Karenina is that it doesn't deal with when you add annotations below where the user is looking at the moment. Those annotations will float in mid-air before dropping because they are moved into the user's visible map rect.

Another is that it also drops the user location blue dot. With this code below, you handle both user location and large amounts of map annotations off-screen. I've also added a nice bounce ;)

- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
    MKAnnotationView *aV; 

    for (aV in views) {

        // Don't pin drop if annotation is user location
        if ([aV.annotation isKindOfClass:[MKUserLocation class]]) {
            continue;
        }

        // Check if current annotation is inside visible map rect, else go to next one
        MKMapPoint point =  MKMapPointForCoordinate(aV.annotation.coordinate);
        if (!MKMapRectContainsPoint(self.mapView.visibleMapRect, point)) {
            continue;
        }

        CGRect endFrame = aV.frame;

        // Move annotation out of view
        aV.frame = CGRectMake(aV.frame.origin.x, aV.frame.origin.y - self.view.frame.size.height, aV.frame.size.width, aV.frame.size.height);

        // Animate drop
        [UIView animateWithDuration:0.5 delay:0.04*[views indexOfObject:aV] options: UIViewAnimationOptionCurveLinear animations:^{

            aV.frame = endFrame;

        // Animate squash
        }completion:^(BOOL finished){
            if (finished) {
                [UIView animateWithDuration:0.05 animations:^{
                    aV.transform = CGAffineTransformMakeScale(1.0, 0.8);

                }completion:^(BOOL finished){
                    if (finished) {
                        [UIView animateWithDuration:0.1 animations:^{
                            aV.transform = CGAffineTransformIdentity;
                        }];
                    }
                }];
            }
        }];
    }
}
Inexpert answered 12/8, 2011 at 20:20 Comment(0)
U
48

Implement the didAddAnnotationViews delegate method and do the animation yourself:

- (void)mapView:(MKMapView *)mapView 
          didAddAnnotationViews:(NSArray *)annotationViews
{
    for (MKAnnotationView *annView in annotationViews)
    {
        CGRect endFrame = annView.frame;
        annView.frame = CGRectOffset(endFrame, 0, -500);
        [UIView animateWithDuration:0.5 
                animations:^{ annView.frame = endFrame; }];
    }
}
Uninterested answered 24/7, 2011 at 20:23 Comment(3)
Thanks, that worked! How would I make it bounce instead of drop in from top?Unroot
You can use all the UIView animation techniques with the annotation view (it's a subclass of UIView). The above example just changes the y coordinate but you can do other transforms. Search SO for something like "uiview bounce animation" for examples. Here's one: https://mcmap.net/q/130844/-mimic-uialertview-bounce/…. You'll need to add QuartzCore framework, import it, and change "view" to "annView".Uninterested
@AnnaKarenina but how can we add CustomView as our pinView and customCallOut any references.Octopod
P
8

@MrAlek's Answer for swift3

optional func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
    print(#function)

    var i = -1;
    for view in views {
        i += 1;
        if view.annotation is MKUserLocation {
            continue;
        }

        // Check if current annotation is inside visible map rect, else go to next one
        let point:MKMapPoint  =  MKMapPointForCoordinate(view.annotation!.coordinate);
        if (!MKMapRectContainsPoint(self.mapView.visibleMapRect, point)) {
            continue;
        }

        let endFrame:CGRect = view.frame;

        // Move annotation out of view
        view.frame = CGRect(origin: CGPoint(x: view.frame.origin.x,y :view.frame.origin.y-self.view.frame.size.height), size: CGSize(width: view.frame.size.width, height: view.frame.size.height))

        // Animate drop
        let delay = 0.03 * Double(i)
        UIView.animate(withDuration: 0.5, delay: delay, options: UIViewAnimationOptions.curveEaseIn, animations:{() in
            view.frame = endFrame
            // Animate squash
            }, completion:{(Bool) in
                UIView.animate(withDuration: 0.05, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations:{() in
                    view.transform = CGAffineTransform(scaleX: 1.0, y: 0.6)

                    }, completion: {(Bool) in
                        UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations:{() in
                            view.transform = CGAffineTransform.identity
                            }, completion: nil)
                })

        })
    }
}
Prakash answered 9/11, 2016 at 2:37 Comment(0)
B
5

@mrAlek answer in Swift:

func mapView(mapView: MKMapView!, didAddAnnotationViews views: [AnyObject]!) {
    println("didAddAnnotationViews()")

    var i = -1;
    for view in views {
        i++;
        let mkView = view as! MKAnnotationView
        if view.annotation is MKUserLocation {
            continue;
        }

        // Check if current annotation is inside visible map rect, else go to next one
        let point:MKMapPoint  =  MKMapPointForCoordinate(mkView.annotation.coordinate);
        if (!MKMapRectContainsPoint(self.mapView.visibleMapRect, point)) {
            continue;
        }

        let endFrame:CGRect = mkView.frame;

        // Move annotation out of view
        mkView.frame = CGRectMake(mkView.frame.origin.x, mkView.frame.origin.y - self.view.frame.size.height, mkView.frame.size.width, mkView.frame.size.height);

        // Animate drop
        let delay = 0.03 * Double(i)
        UIView.animateWithDuration(0.5, delay: delay, options: UIViewAnimationOptions.CurveEaseIn, animations:{() in
            mkView.frame = endFrame
            // Animate squash
            }, completion:{(Bool) in
                        UIView.animateWithDuration(0.05, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations:{() in
                            mkView.transform = CGAffineTransformMakeScale(1.0, 0.6)

                        }, completion: {(Bool) in
                                UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations:{() in
                                    mkView.transform = CGAffineTransformIdentity
                                    }, completion: nil)
                        })

                    })
    }
}
Babushka answered 12/2, 2015 at 18:25 Comment(0)
W
5

@MrAlek's answer in Swift 2:

func mapView(mapView: MKMapView, didAddAnnotationViews views: [MKAnnotationView]) {
    print(__FUNCTION__)

    var i = -1;
    for view in views {
        i++;
        if view.annotation is MKUserLocation {
            continue;
        }

        // Check if current annotation is inside visible map rect, else go to next one
        let point:MKMapPoint  =  MKMapPointForCoordinate(view.annotation!.coordinate);
        if (!MKMapRectContainsPoint(self.mapView.visibleMapRect, point)) {
            continue;
        }

        let endFrame:CGRect = view.frame;

        // Move annotation out of view
        view.frame = CGRectMake(view.frame.origin.x, view.frame.origin.y - self.view.frame.size.height, view.frame.size.width, view.frame.size.height);

        // Animate drop
        let delay = 0.03 * Double(i)
        UIView.animateWithDuration(0.5, delay: delay, options: UIViewAnimationOptions.CurveEaseIn, animations:{() in
            view.frame = endFrame
            // Animate squash
            }, completion:{(Bool) in
                UIView.animateWithDuration(0.05, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations:{() in
                    view.transform = CGAffineTransformMakeScale(1.0, 0.6)

                    }, completion: {(Bool) in
                        UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations:{() in
                            view.transform = CGAffineTransformIdentity
                            }, completion: nil)
                })

        })
    }
}
Weaverbird answered 23/1, 2016 at 18:30 Comment(1)
This is brilliant! Thanks a lot ;)Mangosteen
H
2

Updated for Swift 4.2

   func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
    var i = -1;
    for view in views {
        i += 1;
        if view.annotation is MKUserLocation {
            continue;
        }
        let point:MKMapPoint  =  MKMapPoint(view.annotation!.coordinate);
        if (!self.mapView.visibleMapRect.contains(point)) {
            continue;
        }
        let endFrame:CGRect = view.frame;
        view.frame = CGRect(origin: CGPoint(x: view.frame.origin.x,y :view.frame.origin.y-self.view.frame.size.height), size: CGSize(width: view.frame.size.width, height: view.frame.size.height))
        let delay = 0.03 * Double(i)
        UIView.animate(withDuration: 0.5, delay: delay, options: UIView.AnimationOptions.curveEaseIn, animations:{() in
            view.frame = endFrame
        }, completion:{(Bool) in
            UIView.animate(withDuration: 0.05, delay: 0.0, options: UIView.AnimationOptions.curveEaseInOut, animations:{() in
                view.transform = CGAffineTransform(scaleX: 1.0, y: 0.6)
            }, completion: {(Bool) in
                UIView.animate(withDuration: 0.3, delay: 0.0, options: UIView.AnimationOptions.curveEaseInOut, animations:{() in
                    view.transform = CGAffineTransform.identity
                }, completion: nil)
            })
        })
    }
}
Hoxha answered 15/11, 2018 at 4:55 Comment(0)
A
2

Rather than implementing mapView(_:didAdd:) in the MKMapViewDelegate, you can also have the annotation view do the animation itself.

class CustomAnnotationView: MKAnnotationView {
    override var annotation: MKAnnotation? { didSet { update(for: annotation) } }

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        update(for: annotation)
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        removeFromSuperview()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func didMoveToSuperview() {
        super.didMoveToSuperview()

        transform = CGAffineTransform(translationX: 0, y: -100)
        alpha = 0
        UIViewPropertyAnimator(duration: 0.5, dampingRatio: 0.4) {
            self.transform = .identity
            self.alpha = 1
        }.startAnimation()
    }

    private func update(for annotation: MKAnnotation?) {
        // do whatever update to the annotation view you want, if any
    }
}

This is useful for avoiding the cluttering of one’s view controller with annotation view animations. E.g. in iOS 11 and later, you might do:

mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

And then you can add an annotation to your map view, and you get this animation of the annotation view without any more code in the view controller.

This particular animation is a drop with a little bounce at the end, but obviously you can do whatever animation you want.

The above was written in Swift, but the concept is equally valid in Objective-C, too.

Absorbefacient answered 21/10, 2020 at 15:44 Comment(1)
Great answer! Code worked perfectly with Xcode 14.3.Progressionist

© 2022 - 2024 — McMap. All rights reserved.