9

I have a MKMapView. I added a UITapGestureRecognizer with a single tap.

I now want to add a MKAnnotationView to the map. I can tap the annotation and mapView:mapView didSelectAnnotationView:view fires (which is where I'll add additional logic to display a UIView).

The issue is now when I tap the annotation, the MKMapView tap gesture also fires.

Can I set it so if I tap the annotation, it only responds?

Padin215
  • 7,444
  • 13
  • 65
  • 103

6 Answers6

6

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.

HepaKKes
  • 1,555
  • 13
  • 22
  • Ok. This works. Not completely sure what the `MKAnnotationContainerView` is though. I was expecting the `UIView *v` hitTest to be a MKMapView and was surprised that it isn't. Thank you very much for your help, I really appreciate it. Out of curiosity, do you use xCode? I am unfamiliar with the second method header. – Padin215 Jun 20 '13 at 14:05
  • If I understood well, the `MKAnnotationContainerView` should be an apple private class whose instance should contain the `MKMapView`'s annotationViews. I guess this is the reason why the `hitTest` returns it instead of the `MKMapView`. Regarding your curiosity, yes, I use xcode, probably that signature sounds somewhat strange to you since it is relevant to the C language on which obj C is actually based. – HepaKKes Jun 20 '13 at 14:30
  • Hrm... if its a private class, would apple have issues when I try to upload? – Padin215 Jun 20 '13 at 14:33
  • I don't think so, since your app is not trying to manipulate that object or using a private API it should be ok. for further detail go to this [link](http://stackoverflow.com/questions/3186648/does-apple-view-the-actual-source-code-when-approving-apps) – HepaKKes Jun 20 '13 at 15:03
  • We are not supposed to know about the MKAnnotationContainerView, what if Apple decides to change this API? As of now they actually have, it's now called MKNewAnnotationContainerView. – Rodrigo Ruiz Jul 27 '14 at 22:27
  • @RodrigoRuiz I started with answering that it might not be the best solution...That's why I further edited my answer. If you have a more elegant & robust solution please share with us :) – HepaKKes Jul 27 '14 at 22:59
4

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
}
Sandy Chapman
  • 11,133
  • 3
  • 58
  • 67
2

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);
}
Frane Poljak
  • 2,315
  • 23
  • 25
1

Warning! The accepted solution and also the one below is sometimes bit buggy. Why? Sometimes you tap annotation but your code will act like if you tapped the map. What is the reason of this? Because you tapped somewhere around your frame of your annotation, like +- 1-6 pixels around but not within frame of annotation view.

Interesting also is, that while your code will say in such case "you tapped map, not annotation" default code logic on MKMapView will also accept this close tap, like if it was in the annotation region and will fire didSelectAnnotation.

So you have to reflect this issue also in your code. Lets say this is the default code:

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    CGPoint p = [gestureRecognizer locationInView:_customMapView];

    UIView *v = [_customMapView hitTest:p withEvent:nil];

    if (![v isKindOfClass:[MKAnnotationView class]])
    {
      return YES; // annotation was not tapped, let the recognizer method fire
    }

    return NO;
}

And this code takes in consideration also some proximity touches around annotations (because as said, MKMapView also accepts the proximity touches, not only correct touches):

I included the Log functions so you can watch it in console and understand the problem.

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    CGPoint p = [gestureRecognizer locationInView:_customMapView];
    NSLog(@"point %@", NSStringFromCGPoint(p));

    UIView *v = [_customMapView hitTest:p withEvent:nil];

    if (![v isKindOfClass:[MKAnnotationView class]])
    {
       // annotation was not tapped, be we will accept also some
       // proximity touches around the annotations rects

       for (id<MKAnnotation>annotation in _customMapView.annotations)
       {
           MKAnnotationView* anView = [_customMapView viewForAnnotation: annotation];

           double dist = hypot((anView.frame.origin.x-p.x), (anView.frame.origin.y-p.y)); // compute distance of two points
           NSLog(@"%@ %f %@", NSStringFromCGRect(anView.frame), dist, [annotation title]);
           if (dist <= 30) return NO; // it was close to some annotation se we believe annotation was tapped
       }
       return YES;
    }

    return NO;
}

My annotation frame has 25x25 size, that's why I accept distance of 30. You can apply your logic like if (p.x >= anView.frame.origin.x - 6) && Y etc..

luky
  • 2,263
  • 3
  • 22
  • 40
0

It'd be much easier if we just test the superviews of the touch.view in the gesture delegate:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
    var tv = touch.view
    while let view = tv, !(view is MKAnnotationView) {
        tv = view.superview
    }
    return tv == nil
}
Unheilig
  • 16,196
  • 193
  • 68
  • 98
-1

I'm not sure why you would have a UITapGestureRecognizer on your map view, saying this in plain text is obviously implying it will mess around with some multitouch functionality of your map.

I would suggest you take a look and play around with the cancelsTouchesInView property of UIGestureRecognizer (see documentation). I think this could solve your problem. Make sure you check out the documentation.

Daniel
  • 23,129
  • 12
  • 109
  • 154
  • I draw MKPolygon's on the map which the user interacts with. The `UITapGestureRecognizer` lets the user select a polygon object and do stuff with it. – Padin215 Jun 20 '13 at 13:34
  • Ok, I added a `UITapGestureRecognzier` to the `MKAnnotationView` and set `cancelsTouchesInView`. It did not prevent the tap recognizer on the `MKMapView` from firing. – Padin215 Jun 20 '13 at 15:23