Because the problem is only occurring in iOS 7, you can use one of the new delegate methods to resolve the issue:
– gestureRecognizer:shouldRequireFailureOfGestureRecognizer:
– gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:
I resolved it by implementing gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer
and "crawling" up the gesture's view's superview so I could return "YES" if I find the superview's gesture is equal to the one provided. I detail my full resolution here: https://stackoverflow.com/a/19659848/1147934.
Explanation
The problem with gesture recognizers in iOS 7 is that a superview's gesture is receiving its touches before one of its subview gestures receives its touches. This causes the superview gesture to recognize which then cancels out the sub view's recognizer... this is (incorrect?) and multiple bugs have been filed with Apple. It's been pointed out that Apple doesn't guarantee the order in which gestures receive touches. I think a lot of "us" have been relying on an implementation detail that changed in iOS 7. This is why we use the new delegate methods, which seem designed to help us address this problem.
Note: I did extensive testing by using my own sublcassed recognizers, logging all touches and discovered that the reason recognizers fail is because superview gestures were receiving touches before a subview's gesture was in about ~5% of the cases. Every time this happened, failure occurred. This does happen more often if you have "deep" hierarchies with lots of gestures.
The new delegate methods can be confusing, so you need to read them carefully.
I'm using the method (I've renamed the arguments to make them easier to understand)
– gestureRecognizer:(UIGestureRecognizer *)thisRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *) otherRecognizer
.
If you return "YES", then the gesture recognizer provided, otherRecognizer
, will require thisRecognizer
to fail before it can be recognized. This is why, in my answer, I crawl up the superview hierarchy to check if it contains a superview that has the otherRecognizer
. If it does, I want otherRecognizer
to require thisRecognizer
to fail because thisRecognizer
is in a subview and should fail before it's superview's gesture is recognized. This will make sure that subview gestures are recognized before their superview's gestures are. Make sense?
Alternative
I could go about it the other way around and use:
– gestureRecognizer:(UIGestureRecognizer *)thisRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherRecognizer
Now I would need to crawl through my entire subview hierarchy, check if otherRecognizer
is in it and return YES
if it is. I don't use this method because crawling the entire subview hierarchy is much more difficult and expensive to do than to check a superview hierarchy. Crawling a subview hierarchy would have to be a recursive function, while I can use a simple while
loop to check a superview's hierarchy. So I recommend the first approach I outline.
Warning!
Be careful about using gestureRecognizer:shouldReceiveTouch:
. The issue is a problem of which gesture receives touches first (canceling the other gesture out)... it's a problem of conflict resolution. If you implement gestureRecognizer:shouldReceiveTouch:
, you risk rejecting a superview's gesture if the subview gesture fails because you have to guess when a subview gesture might be recognized. A subview gesture may legitimately fail for reasons other than the touches are out of bounds, so you would have to know implementation details in order to guess correctly. You want the superview gesture to be recognized when the subview gesture fails but you don't really have anyway to know for certain if it will fail before it actually fails. If a subview gesture fails, normally you want the superview gesture to then recognize. This is the normal responder chain (subview superview) and if you mess with that you could end up with unexpected behavior.