简体   繁体   中英

Problems with gesture recognizer in iOS 7

I'm adding several UIView objects (eg 5) to the screen, one inside the other. This, for example, view5.superview = view4 , view4.superview = view3 , view3.superview=view2 , view2.superview = view1 . For all these UIView I set uitapgesturerecognizer; for view1-4 I just do NSLog(@"tap %@", self) in callback, while for view5 tap I set the following: delete view4 from the hierarchy, then put the same object view4' at the same place of the hierarchy. This object also contains view5' for which UITapGestureRecognizer is set (practically, I replace one part of markup with similar one).

Then I start clicking on view5. Some time view5 keeps catching its tap and everything's OK, but random number of taps later (every time this number is different) one of the view1-4 objects starts catching this tap, though we're still clicking the view5. The whole problem has a random character - sometimes it occurs at the 10th launch, sometimes at the second. Sometimes wrong objects start catching taps at the first tap. Also I never know what object will catch a tap, when everything goes wrong. The frame for view(n+1) was set, eg, as a half of the frame view(n), while the frame for view1 - eg (0,0 320, 460).

All operations with ui objects described above are conducted in the main thread, and everything I've told about perfectly worked on iOS 4.3 - 6.1 with much more complex examples. But the iOS7 makes out of it some kind of a random hell.

Update: I've created a sample project, to simplify the debug process. No add/remove subview operations on tap. Only 4 views on screen, on tap the app logs what view was tapped. So, you need to tap on smallest view (4). If you see "tap 4 tap 4 tap 4…" in the log - this is the case when everything works fine, stop and run again, stop and run again, stop and run again, etc. And at some runs (maybe after 10+ successful runs) you won't see "tap 4" on the first line, you will see "tap 1" or "tap 2" or "tap 3", and it will continue so - these are the bad cases.

Sample project can be downloaded from here: http://tech.octopod.com/test/BuggySample.zip (just 33 Kb in archive).

Update 2

We've posted a bug to Apple, I'll post here when we will get some feedback. However, any good workaround would be much appreciated!

Update 3

Solution, provided by Yuvrajsinh is really working on the sample project. Unfortunately, it still does not help to solve the problem that occurred in the main project where it initially appeared. The main reason for now is that if any view without self gesture is laying upon the clickable content, random view element under it starts catching the interaction (instead of the top one with interaction gesture set. Do you have any ideas how it can be solved? The updated sample can be downloaded from here: http://tech.octopod.com/test/BuggySample2.zip

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.

I have made some changes in your code and I also have tested it much and problem is not generating.

While creating view I set tag to each view to distinguish it:

View1234 *v1 = [[View1234 alloc] initWithId:@"1"];
v1.tag =1;
v1.backgroundColor = [UIColor redColor];
v1.frame = CGRectMake(0, 0, 320, 460);

View1234 *v2 = [[View1234 alloc] initWithId:@"2"];
v2.tag=2;
v2.backgroundColor = [UIColor greenColor];
v2.frame = CGRectMake(0, 0, 160, 230);

View1234 *v3 = [[View1234 alloc] initWithId:@"3"];
v3.tag=3;
v3.backgroundColor = [UIColor blueColor];
v3.frame = CGRectMake(0, 0, 80, 115);

View1234 *v4 = [[View1234 alloc] initWithId:@"4"];
v4.tag=4;
v4.backgroundColor = [UIColor orangeColor];
v4.frame = CGRectMake(0, 0, 40, 50);

View1234.h

@interface View1234 : UIView <UIGestureRecognizerDelegate> {
    NSString *vid;
}

- (id) initWithId:(NSString *)vid_;
@end

And following is whole code of View1234.m

- (id) initWithId:(NSString *)vid_ {
    if (self = [super init]) {

        vid = [vid_ retain];

        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] init];
        tapGesture.delegate = self;
        tapGesture.numberOfTapsRequired = 1;
        tapGesture.numberOfTouchesRequired = 1;
        [tapGesture addTarget:self action:@selector(handleTap:)];

        [self addGestureRecognizer:tapGesture];

        [tapGesture release];

    }

    return self;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
    if (touch.view==self ) {
        return YES;
    }
    else if ([self.subviews containsObject:touch.view] && ![touch.view isKindOfClass:[self class]])
    {
        return YES;
    }
    return NO;
}

/*- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{

    if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
        if (self.tag==gestureRecognizer.view.tag) {
            return YES;
        }
    }

    return NO;
}*/

- (void) handleTap:(UITapGestureRecognizer *)tap {
    NSLog(@"tap %@", vid);
}

- (void) dealloc {

    [vid release];
    vid = nil;

    [super dealloc];
}

UPDATE : Why This problem comes actually.

When you add a UIView as a subview of another UIView with UITapGestureRecognizer in each view, then in some rare case UITapGestureRecognizer state becomes Failed somehow ( I have debug it more than 50 times and come to know this ). So when any subview of any view is not able to handle the tap gesture then system will pass gesture to it's super view to handle that gesture, and this continues.

If you debug then you will come to know that gestureRecognizerShouldBegin is called multiple times as per hierarchy of view. In this particular case if I tap on view3 then gestureRecognizerShouldBegin will call 3 times as view3 is on 3rd level of view hierarchy, so gestureRecognizerShouldBegin will be called for view3, view2 and view1 .

So to solve problem I am returning YES form gestureRecognizerShouldBegin for the correct view and NO for the rest, so it solves problem.

UPDATE 2 : I have made some change in code in my edited answer and hope will solve your problem. And also thanks to @masmor, I also found some clue from his answer to make problem solve.

Set a delegate on the recognizer and implement gestureRecognizer:shouldReceiveTouch: .

The implementation should basically block touches on subviews, but there will probably be some additional criteria according to your actual view hierarchy and setup.

The sample below simply checks if the hit view is a direct subview and if it has any gesture recognizers, in which case it is allowed to see the touches.

Blocking touches should be more robust than fiddling with recognizer states, as there is no chance for any unwanted views to fire their recognizers. The requirement of implementing custom criteria is a drawback, but again I think it's more robust to explicitly implement behavior when working around a bug of unknown cause like in this case.

#import "View1234.h"

@interface View1234 () <UIGestureRecognizerDelegate>

@end

@implementation View1234

- (id) initWithId:(NSString *)vid_ {
    if (self = [super init]) {

        vid = [vid_ retain];

        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] init];
        tapGesture.numberOfTapsRequired = 1;
        tapGesture.numberOfTouchesRequired = 1;
        [tapGesture addTarget:self action:@selector(handleTap:)];

        tapGesture.delegate = self;

        [self addGestureRecognizer:tapGesture];

        [tapGesture release];
    }

    return self;
}

//- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
//    
//    if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
//        if (self.tag==gestureRecognizer.view.tag) {
//            return YES;
//        }
//    }
//    
//    return NO;
//}

- (void) handleTap:(id)tap {
    NSLog(@"tap %@", vid);
}

- (void) dealloc {

    [vid release];
    vid = nil;

    [super dealloc];
}

- (BOOL)shouldReceiveTouchOnView:(UIView *)hitView {
  NSLog(@"Should view:\n%@\nreceive touch on view:\n%@", self, hitView);

  // Replace this implementation with whatever you need...
  // Here, we simply check if the view has a gesture recognizer and
  // is a direct subview.
  BOOL res = (hitView.gestureRecognizers.count == 0 &&
              [self.subviews containsObject:hitView]);

  NSLog(@"%@", res? @"YES":@"NO");

  return res;
}

#pragma mark - Gesture Recognizer Delegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
       shouldReceiveTouch:(UITouch *)touch {
  UIView *hitView = [self hitTest:[touch locationInView:self.superview]
                        withEvent:nil];
  if (hitView == self) {
    NSLog(@"Touch not in subview");
    return YES;
  }

  return [self shouldReceiveTouchOnView:hitView];
}

@end

I haven't tried your project or the below.

You should be able to use gestureRecognizerShouldBegin: to prevent any gesture which doesn't belong to the view from firing when the view is touched.

You could do that with a subclass of UIView , or you could create a category on UIView (with a property or associated object) which adds a flag to determine what each view instance should do - this will break some view types so beware.

That won't help if the problem is the order of the views...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM