MKAnnotationView and tap detection
Asked Answered
P

6

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?

Papeete answered 19/6, 2013 at 20:41 Comment(0)
O
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.

Oakum answered 20/6, 2013 at 0:19 Comment(6)
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.Papeete
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.Oakum
Hrm... if its a private class, would apple have issues when I try to upload?Papeete
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 linkOakum
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.Internal
@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 :)Oakum
S
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
}
Smallman answered 4/1, 2016 at 13:51 Comment(0)
C
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);
}
Captious answered 11/2, 2015 at 15:59 Comment(0)
D
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..

Desmoid answered 15/8, 2016 at 13:32 Comment(0)
M
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
}
Madeira answered 31/10, 2018 at 2:31 Comment(0)
B
-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.

Burgoo answered 19/6, 2013 at 21:36 Comment(2)
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.Papeete
Ok, I added a UITapGestureRecognzier to the MKAnnotationView and set cancelsTouchesInView. It did not prevent the tap recognizer on the MKMapView from firing.Papeete

© 2022 - 2024 — McMap. All rights reserved.