25

UIView在屏幕上添加了几个对象(例如 5 个),一个在另一个里面。例如,view5.superview = view4, view4.superview = view3, view3.superview=view2, view2.superview = view1. 对于所有这些UIView,我设置了 uitapgesturerecognizer;对于 view1-4 我只是在回调中执行 NSLog(@"tap %@", self) ,而对于 view5 点击我设置以下内容:从层次结构中删除 view4,然后将相同的对象 view4' 放在层次结构的同一位置. 该对象还包含为其UITapGestureRecognizer设置的 view5'(实际上,我将标记的一部分替换为类似的部分)。

然后我开始点击view5。有时 view5 一直在捕捉它的点击,一切正常,但随后随机点击数次(每次这个数字不同),其中一个 view1-4 对象开始捕捉到这个点击,尽管我们仍在点击 view5。整个问题具有随机性——有时发生在第 10 次启动时,有时发生在第 2 次启动时。有时错误的对象在第一次点击时开始捕捉点击。此外,当一切都出错时,我也不知道什么物体会被点击。视图(n+1) 的帧被设置为例如帧视图(n) 的一半,而视图1 的帧-例如(0,0 320, 460)。

上面描述的所有对 ui 对象的操作都在主线程中进行,我所说的一切都在 iOS 4.3 - 6.1 上完美运行,并带有更复杂的示例。但是 iOS7 让它成为了某种随机的地狱。

更新: 我创建了一个示例项目,以简化调试过程。没有点击添加/删除子视图操作。屏幕上只有 4 个视图,点击应用程序会记录被点击的视图。因此,您需要点击最小视图 (4)。如果您在日志中看到“tap 4 tap 4 tap 4…” - 这是一切正常的情况,停止并再次运行,停止并再次运行,停止并再次运行,等等。并且在某些运行时(可能在 10 +成功运行)你不会在第一行看到“tap 4”,你会看到“tap 1”或“tap 2”或“tap 3”,它会继续这样 - 这些都是坏情况。

示例项目可以从这里下载: http: //tech.octopod.com/test/BuggySample.zip(存档中只有 33 Kb)。

更新 2

我们已经向 Apple 发布了一个错误,当我们得到一些反馈时我会在这里发布。但是,任何好的解决方法将不胜感激!

更新 3

Yuvrajsinh 提供的解决方案确实在示例项目上工作。不幸的是,它仍然无助于解决最初出现的主项目中出现的问题。现在的主要原因是,如果任何没有自我手势的视图放在可点击的内容上,它下面的随机视图元素就会开始捕捉交互(而不是带有交互手势集的顶部。你有什么想法可以解决吗? ? 可以从这里下载更新的示例:http: //tech.octopod.com/test/BuggySample2.zip

4

4 回答 4

19

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.

于 2013-11-10T16:34:14.083 回答
12

我已经对您的代码进行了一些更改,并且我也对其进行了很多测试,并且没有产生问题。

在创建视图时,我为每个视图设置了标签以区分它:

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);

查看1234.h

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

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

以下是完整的代码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];
}

更新:为什么这个问题实际上来了。

当您在每个视图UIView中添加一个作为另一个的子视图时UIViewUITapGestureRecognizer在极少数情况下,UITapGestureRecognizer状态会以某种方式变为失败我已经对其进行了 50 多次调试并知道这一点)。因此,当任何视图的任何子视图都无法处理点击手势时,系统会将手势传递给它的超级视图以处理该手势,然后继续。

如果您调试,那么您将知道gestureRecognizerShouldBegin根据视图层次结构多次调用它。在这种特殊情况下,如果我点击view3thengestureRecognizerShouldBegin将调用3 次,就像view3在视图层次结构的第 3 级一样,所以gestureRecognizerShouldBegin将调用view3, view2 and view1.

因此,为了解决问题,我将返回正确视图和其余部分的YES表单,这样它就解决了问题。gestureRecognizerShouldBeginNO

更新 2:我在编辑的答案中对代码进行了一些更改,希望能解决您的问题。还要感谢@masmor,我还从他的回答中找到了一些线索来解决问题。

于 2013-10-16T06:39:24.670 回答
2

在识别器上设置一个委托并实现gestureRecognizer:shouldReceiveTouch:.

实现应该基本上阻止对子视图的触摸,但根据您的实际视图层次结构和设置,可能会有一些额外的标准。

下面的示例只是检查命中视图是否是直接子视图以及它是否有任何手势识别器,在这种情况下,它可以看到触摸。

阻止触摸应该比摆弄识别器状态更健壮,因为任何不需要的视图都没有机会触发它们的识别器。实现自定义标准的要求是一个缺点,但我再次认为,在解决像这种情况下的未知原因的错误时,显式地实现行为更加健壮。

#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
于 2013-10-18T06:50:25.353 回答
0

我还没有尝试过您的项目或以下项目。

您应该能够使用它gestureRecognizerShouldBegin:来防止在触摸视图时触发任何不属于视图的手势。

您可以使用 的子类来做到这一点,或者您可以在(带有属性或关联对象)UIView上创建一个类别,该类别添加一个标志来确定每个视图实例应该做什么 - 这会破坏一些视图类型,所以要小心。UIView

如果问题是视图的顺序,那将无济于事......

于 2013-10-11T19:57:19.823 回答