For several years now I've operated under the assumption that if a superview and its subview both have gesture recognizers, the subview would receive the touches first and cancel out the superview's gesture. Until iOS 7 this assumption never failed me, allowing me to add gestures to subviews confident the superview's gestures wouldn't interfere. But in iOS 7, the superview will randomly receive the touches first and cancel out the subview's gestures. This happens somewhat rarely, which made the problem hard to spot.
I first experienced this problem as buttons that couldn't be tapped using UITapGestureRecognizer
... again, very rarely. Usually the buttons would work until they didn't. Made you kind of question your sanity. So I rolled my own TapGestureRecognizer
and discovered that superview taps were canceling their subview's taps on occasion. It's never done this in any previous version of iOS, but I'm wondering if this behavior was simply never promised.
I thought the subview's gesture was supposed to cancel it's superview's gesture (unless otherwise specified by a delegate). Is this wrong or is this a bug?
Please Note: I'm not asking how to handle the situation. I'm asking if anyone knows whether my assumption is incorrect. I'm already rearranging views, dynamically adding/removing gestures and creating rather complex implementations of gestureRecognizer:shouldReceiveTouch:
to remedy the situation. It's not fun, but I can work around the problem.
After a lot of searching around I found a discussion on Apple's message boards with other users that are having this problem: Issues with UITapGestureRecognizer (developer account required). I went ahead and submitted a bug report: 15331126 (does anyone know how to link to bug reports anymore?). In the meantime, I implemented this workaround. So far it seems to be working, but since the bug is so rare I may simply not have triggered it yet. I'm releasing it to my beta users and if I get no complaints from them (who have been complaining) I'll assume this fixes the issue.
UPDATE:
This solution has fixed the problem. After weeks of use by dozens of users I haven't had a single issue with gestures.
Most of my gestures are custom. I altered them to be delegates of themselves and implemented:
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if (gestureRecognizer == self){
if ([otherGestureRecognizer isMemberOfClass:self.class]){
if ([self isGestureRecognizerInSuperviewHierarchy:otherGestureRecognizer]){
return YES;
} else if ([self isGestureRecognizerInSiblings:otherGestureRecognizer]){
return YES;
}
}
}
return NO;
}
Note that my custom gestureRecognizers implement the UIGestureRecognizerDelegate protocol now (publically, for reasons you'll see below). I also added a couple of categories to UIGestureRecognizer (used in the above code):
- (BOOL) isGestureRecognizerInSiblings:(UIGestureRecognizer *)recognizer{
UIView *superview = self.view.superview;
NSUInteger index = [superview.subviews indexOfObject:self.view];
if (index != NSNotFound){
for (int i = 0; i < index; i++){
UIView *sibling = superview.subviews[i];
for (UIGestureRecognizer *viewRecognizer in sibling.gestureRecognizers){
if (recognizer == viewRecognizer){
return YES;
}
}
}
}
return NO;
}
- (BOOL) isGestureRecognizerInSuperviewHierarchy:(UIGestureRecognizer *)recognizer{
if (!recognizer) return NO;
if (!self.view) return NO;
//Check siblings
UIView *superview = self.view;
while (YES) {
superview = superview.superview;
if (!superview) return NO;
for (UIGestureRecognizer *viewRecognizer in superview.gestureRecognizers){
if (recognizer == viewRecognizer){
return YES;
}
}
}
}
I'm not entirely sure that I need to check for siblings as I've only seen the issue occur with superview gestures. However, I didn't want to take that chance. Note that I only check for siblings "below" the current one as I don't want to cancel view gestures "above" the current view.
I had to add implementations for for those classes that set themselves as delegates of the custom recognizers, but they pretty much just call back to the gestureRecognizer:
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if ([gestureRecognizer respondsToSelector:@selector(gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:)]){
return [(id <UIGestureRecognizerDelegate>)gestureRecognizer gestureRecognizer:gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:otherGestureRecognizer];
}
return NO;
}
Hope this helps anyone else having the problem.