0
votes

I encountered a weird bug (or I'm really stupid...): I wanna display a custom callout in an MKAnnotationView. What I'm doing is adding a custom view as a subview of the MKAnnotationView. I'm having problems with the adding animation.

I have a MKMapView with my view controller as delegate

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)annotationView
{
    [self.calloutViewController setCalloutState:CalloutStateNormal
                                       animated:YES
                                 annotationView:annotationView
                                     completion:NULL];
}

- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)annotationView
{
    [self.calloutViewController setCalloutState:CalloutStateHidden
                                       animated:YES
                                 annotationView:annotationView
                                     completion:NULL];
}

self.calloutViewController is a custom view controller that creates a view with bubble shape (just adding round corners and white background).

- (void)setCalloutState:(CalloutState)calloutState
               animated:(BOOL)animated
         annotationView:(MKAnnotationView *)annotationView
             completion:(void (^)(BOOL finished))completion
{
    if (self.view.superview == annotationView || !annotationView) {

    } else {
//        [self.view.layer removeAllAnimations];
        [self.view removeFromSuperview];

        self.view.transform = CGAffineTransformIdentity;
        self.view.bounds = CGRectMake(0, 0, normalViewWidth, normalViewHeight);
        self.view.transform = CGAffineTransformMakeScale(0, 0);
        self.view.center = CGPointMake(annotationView.bounds.size.width / 2, 0);

        [annotationView addSubview:self.view];
    }

    void (^animationBlock)(void) = ^{
        self.view.transform = CGAffineTransformIdentity;

        switch (calloutState) {
            case CalloutStateHidden:
                self.view.bounds = CGRectMake(0, 0, normalViewWidth, normalViewHeight);
                self.view.transform = CGAffineTransformMakeScale(0, 0);
                break;
            case CalloutStateNormal:
                self.view.bounds = CGRectMake(0, 0, normalViewWidth, normalViewHeight);
                break;
            case CalloutStateExpanded:
                self.view.bounds = CGRectMake(0, 0, expandedViewWidth, expandedViewHeight);
                break;
            default:
                break;
        }

        self.view.center = CGPointMake(annotationView.bounds.size.width / 2, 0);

        [self.view layoutIfNeeded];
    };

    if (animated) {
        // TODO: figure out why the first animateWithDuration is needed in this nested thing
        [UIView animateWithDuration:0
                              delay:0
                            options:UIViewAnimationOptionBeginFromCurrentState
                         animations:NULL
                         completion:^(BOOL finished) {

                             [UIView animateWithDuration:0.3
                                                   delay:0
                                                 options:UIViewAnimationOptionBeginFromCurrentState
                                              animations:animationBlock
                                              completion:^(BOOL finished) {
                                                  if (finished) {
                                                      self.calloutState = calloutState;

                                                      // TODO: figure out how to end UIView animation instantly so we don't need the second condition at the if
                                                      // having a concurrency problem here
                                                      if (calloutState == CalloutStateHidden && self.view.superview == annotationView) {
                                                          [self.view removeFromSuperview];
                                                      }

                                                      self.editingEnabled = calloutState == CalloutStateExpanded;
                                                  }

                                                  if (completion) {
                                                      completion(finished);
                                                  }
                                              }];

                         }];
        // ---------------------------------------------------------------------------------
    } else {
        animationBlock();

        self.calloutState = calloutState;
        if (calloutState == CalloutStateHidden) {
            [self.view removeFromSuperview];
        }

        self.editingEnabled = calloutState == CalloutStateExpanded;

        if (completion) {
            completion(YES);
        }
    }
}

The problem is: - If I select one pin (MKAnnotationView) on the map it animates correctly showing the callout bubble I created. - After that I tap a different pin (while the bubble was still showing in the last one). - Then with option 1) the callout bubble just jumps from one pin to another, without animating its scale. - With option 2) it works just fine the way it is supposed to.

I wanna understand why option 1) doesn't work. I don't think it should make any difference, since there is no animation going on when I tap the second pin.

Thanks in advance.

EDIT: Just to simplify, I've created a project with the minimum amount of code necessary to reproduce my problem:

@interface TestViewController () <MKMapViewDelegate>

@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (strong, nonatomic) UIView *customCallout;

@end

@implementation TestViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.customCallout = [[UIView alloc] initWithFrame:CGRectMake(-50, -50, 100, 50)];
    self.customCallout.backgroundColor = [UIColor colorWithWhite:1 alpha:0.7];

    self.mapView.delegate = self;

    CustomAnnotation *annoation1 = [CustomAnnotation new];
    annoation1.coordinate = CLLocationCoordinate2DMake(40, -100);
    [self.mapView addAnnotation:annoation1];

    CustomAnnotation *annoation2 = [CustomAnnotation new];
    annoation2.coordinate = CLLocationCoordinate2DMake(40, -90);
    [self.mapView addAnnotation:annoation2];
}

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
    [self.customCallout removeFromSuperview];
    self.customCallout.transform = CGAffineTransformMakeScale(0, 0);
    [view addSubview:self.customCallout];

//    [UIView animateWithDuration:0
//                          delay:0
//                        options:UIViewAnimationOptionBeginFromCurrentState
//                     animations:NULL
//                     completion:^(BOOL finished) {
                         [UIView animateWithDuration:1
                                               delay:0
                                             options:UIViewAnimationOptionBeginFromCurrentState
                                          animations:^{
                                              self.customCallout.transform = CGAffineTransformIdentity;
                                          }
                                          completion:NULL];
//                     }];
}

@end

Just like before, the first time you tap one annotation, it works fine, but when you tap the second annotation the animation doesn't happen. If you uncomment the commented code, it works fine.

1

1 Answers

2
votes

UIViewAnimationOptionBeginFromCurrentState begins the animation from the current state as drawn on screen, not from the current value of the property. So, even though the view's transform property has a value of Scale(0, 0), it hasn't drawn that to screen yet. The reason that the extra animation block makes a difference is that, even with a duration of 0, the completion isn't executed immediately, but rather on the next pass through the run loop, which gives the view enough time to draw itself.

If you remove the UIViewAnimationOptionBeginFromCurrentState option, the animation will work as expected:

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
    [self.customCallout removeFromSuperview];
    self.customCallout.transform = CGAffineTransformMakeScale(0, 0);
    [view addSubview:self.customCallout];
    [UIView animateWithDuration:1 animations:^{
        self.customCallout.transform = CGAffineTransformIdentity;
    }];
}