MKAnnotationView and tap detection

There might be a better and cleaner solution but one way to do the trick is exploiting hitTest:withEvent: in the tap gesture recognized selector, e.g.

suppose you have added a tap gesture recognizer to your _mapView

- (void)tapped:(UITapGestureRecognizer *)g
{
    CGPoint p = [g locationInView:_mapView];
    UIView *v = [_mapView hitTest:p withEvent:nil];

    if (v ==  subviewOfKindOfClass(_mapView, @"MKAnnotationContainerView"))
        NSLog(@"tap on the map"); //put your action here
}

// depth-first search
UIView *subviewOfKindOfClass(UIView *view, NSString *className)
{
    static UIView *resultView = nil;

    if ([view isKindOfClass:NSClassFromString(className)])
        return view;

    for (UIView *subv in [view subviews]) {
        if ((resultView = subviewOfKindOfClass(subv, className)) break;
    }
    return resultView;
}

It's probably doesn't cover all the edge cases but it seems to work pretty well for me.

UPDATE (iOS >= 6.0)

Finally, I found another kind of solution which has the drawback of being valid only for iOS >= 6.0: In fact, this solution exploits the new -(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer added to the UIViews in this way

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    // overrides the default value (YES) to have gestureRecognizer ignore the view
    return NO; 
}

I.e., from the iOS 6 onward, it's sufficient to override that UIView method in each view the gesture recognizer should ignore.


Your solution should be making use of the - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch method on your delegate.

In this method, you can check if the touch was on one of your annotations, and if so, return NO so that your gestureRecognizer isn't activated.

Objective-C:

- (NSArray*)getTappedAnnotations:(UITouch*)touch
{
    NSMutableArray* tappedAnnotations = [NSMutableArray array];
    for(id<MKAnnotation> annotation in self.mapView.annotations) {
        MKAnnotationView* view = [self.mapView viewForAnnotation:annotation];
        CGPoint location = [touch locationInView:view];
        if(CGRectContainsPoint(view.bounds, location)) {
            [tappedAnnotations addObject:view];
        }
    }
    return tappedAnnotations;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    return [self getTappedAnnotations:touch].count > 0;
}

Swift:

private func getTappedAnnotations(touch touch: UITouch) -> [MKAnnotationView] {
    var tappedAnnotations: [MKAnnotationView] = []
    for annotation in self.mapView.annotations {
        if let annotationView: MKAnnotationView = self.mapView.viewForAnnotation(annotation) {
            let annotationPoint = touch.locationInView(annotationView)
            if CGRectContainsPoint(annotationView.bounds, annotationPoint) {
                tappedAnnotations.append(annotationView)
            }
        }
    }
    return tappedAnnotations
}

func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
    return self.getTappedAnnotations(touch: touch).count > 0
}

Why not just add UITapGestureRecognazer in viewForAnnotation, use annotation's reuseIdentifier to identify which annotation it is, and in tapGestureRecognizer action method you can access that identifier.

-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
    MKAnnotationView *ann = (MKAnnotationView*)[mapView dequeueReusableAnnotationViewWithIdentifier:@"some id"];

    if (ann) {
        return ann;
    }

    ann = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"some id"];
    ann.enabled = YES;

    UITapGestureRecognizer *pinTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(pinTapped:)];
    [ann addGestureRecognizer:pinTap];
}

-(IBAction)pinTapped:(UITapGestureRecognizer *)sender {
    MKAnnotationView *pin = (MKPinAnnotationView *)sender.view;
    NSLog(@"Pin with id %@ tapped", pin.reuseIdentifier);
}