I have a MKMapView
. I added a UITapGestureRecognizer
with a single tap.
I now want to add a MKAnnotationView
to the map. I can
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
}
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
}
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);
}
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 UIView
s 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.
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..
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.