允许与另一个UIView下的UIView交互


115

有没有一种简单的方法可以允许与另一个UIView下的UIView中的按钮进行交互-在该UIView的顶部没有真正的对象,而是位于按钮顶部?

例如,目前我有一个UIView(A),在屏幕的顶部是一个对象,在屏幕的底部是一个对象,中间没有任何对象。它位于另一个UIView的顶部,中间有按钮(B)。但是,我似乎无法与B中间的按钮进行交互。

我可以看到B中的按钮-我已经将A的背景设置为clearColor-但是B中的按钮似乎没有接收到触摸,尽管这些按钮之上实际上没有来自A的对象。

编辑 -我仍然希望能够与顶部UIView中的对象进行交互

当然有一个简单的方法可以做到这一点吗?


2
其几乎所有内容都在这里进行了解释:developer.apple.com/iphone/library/documentation/iPhone/… 但基本上,重写hitTest:withEvent :,它们甚至提供了代码示例。
nash

我为此写了一小堂课。(在答案中添加了一个示例)。那里的解决方案比接受的答案要好一些,因为您仍然可以单击UIButton半透明下的,UIView而的非透明部分UIView仍会响应触摸事件。
塞格夫2015年

Answers:


97

您应该为顶视图创建一个UIView子类,并重写以下方法:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // UIView will be "transparent" for touch events if we return NO
    return (point.y < MIDDLE_Y1 || point.y > MIDDLE_Y2);
}

您还可以查看hitTest:event:方法。


19
middle_y1 / y2在这里代表什么?
杰森·雷纳尔多2014年

不知道该return声明在做什么,但return CGRectContainsPoint(eachSubview.frame, point)对我有用。否则很有帮助的答案
n00neimp0rtant 2015年

MIDDLE_Y1 / Y2业务只是一个例子。对于该MIDDLE_Y1<=y<=MIDDLE_Y2区域中的触摸事件,此功能将是“透明的” 。
gyim 2015年

41

尽管这里的许多答案都行得通,但我感到惊讶的是,这里没有给出最方便,通用和最简单的答案。@Ash距离最近,除了返回Superview时发生了一些奇怪的事情……不要那样做。

该答案取自我对这里的类似问题的回答。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView == self) return nil;
    return hitView;
}

[super hitTest:point withEvent:event]将返回该视图层次结构中被触摸的最深视图。如果hitView == self(即,如果有触摸点在无子视图),回报率nil,指定,这种观点应该接收触摸。响应者链的工作方式意味着,将继续遍历此点之上的视图层次结构,直到找到将对触摸做出响应的视图为止。不要返回超级视图,因为它的超级视图是否应接受触摸取决于该视图!

该解决方案是:

  • 方便,因为它不需要引用任何其他视图/子视图/对象;
  • 通用,因为它适用于纯粹充当可触摸子视图容器的任何视图,并且子视图的配置不会影响其工作方式(就像您重写pointInside:withEvent:以返回特定的可触摸区域时一样)。
  • 万无一失,没有太多代码...而且这个概念并不难理解。

我经常使用它,以至于将其抽象为一个子类以保存无意义的视图子类以进行覆盖。作为奖励,添加一个属性使其可配置:

@interface ISView : UIView
@property(nonatomic, assign) BOOL onlyRespondToTouchesInSubviews;
@end

@implementation ISView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView == self && onlyRespondToTouchesInSubviews) return nil;
    return hitView;
}
@end

然后变得疯狂起来,并在可能使用平原的任何地方使用此视图UIView。配置它很简单,只要设置onlyRespondToTouchesInSubviewsYES


2
唯一正确的答案。明确地说,如果您要忽略对视图的触摸,而不要忽略视图中包含的任何按钮(例如),请按照Stuart的说明进行操作。(我通常称其为“持有人视图”,因为它可以无害地“持有”某些按钮,但不会影响持有人“下方”的任何内容。)
Fattie 2014年

如果视图是可滚动的,则其他一些使用点的解决方案也会出现问题,例如UITableView或UICollectionView,并且您已向上或向下滚动。但是,无论滚动如何,此解决方案均有效。
pajevic '16

31

有几种方法可以解决此问题。我最喜欢的是在作为冲突视图的通用超级视图(可能是间接视图)的视图中覆盖hitTest:withEvent :(听起来像您将它们称为A和B)。例如,如下所示(这里A和B是UIView指针,其中B是“隐藏”的指针,通常会被忽略):

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pointInB = [B convertPoint:point fromView:self];

    if ([B pointInside:pointInB withEvent:event])
        return B;

    return [super hitTest:point withEvent:event];
}

您还可以pointInside:withEvent:按照gyim建议修改方法。通过有效地在A中“戳个洞”,至少对于触摸,这可以使您获得基本相同的结果。

另一种方法是事件转发,这意味着可以使用覆盖touchesBegan:withEvent:和类似方法(例如,类似的方法touchesMoved:withEvent:)将某些触摸发送到与其初次到达的位置不同的对象。例如,在A中,您可以这样写:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([self shouldForwardTouches:touches]) {
        [B touchesBegan:touches withEvent:event];
    }
    else {
        // Do whatever A does with touches.
    }
}

但是,这并不总是能达到您期望的效果!最主要的是,诸如UIButton之类的内置控件将始终忽略转发的触摸。因此,第一种方法更可靠。

有一篇很好的博客文章更详细地解释了所有这些内容,还有一个小型的工作中的xcode项目演示了这些想法,可在此处找到:

http://bynomial.com/blog/?p=74


该解决方案的问题是-在公共超级视图上覆盖hitTest-是当底层视图是一组可滚动视图(MapView等)之一时,它不允许底层视图完全正确地工作。据我所知,下面的gyim建议的顶视图内部的覆盖点在所有情况下均适用。
delany 2011年

@delany,那不是真的;您可以将可滚动视图置于其他视图之下,并通过覆盖hitTest使其同时工作。以下是一些示例代码:bynomial.com/blogfiles/Temp32.zip
Tyler

嘿。并非所有可滚动视图-只是一些...我从上面尝试了邮政编码,将UIScrollView更改为(例如)MKMapView,但它不起作用。点按工作-滚动似乎是问题所在。
delany 2011年

好的,我检查了一下并确认hitTest无法按照您希望的MKMapView的方式工作。你是对的,@ delany; 尽管它确实可以与UIScrollView一起使用。我不知道为什么MKMapView失败?
泰勒

@Tyler正如您提到的那样,“最主要的是,像UIButton这样的内置控件将始终忽略转发的触摸”,我想知道您是如何知道的,以及它是否是对此行为进行解释的正式文档。我遇到一个问题,当UIButton具有纯UIView子视图时,它将不会响应子视图范围内的触摸事件。而且我发现该事件已作为UIView的默认行为正确转发到了UIButton,但是我不确定这是一个指定功能还是一个bug。您能告诉我有关此文档的信息吗?真心的谢谢你。
Neal.Marlin,

29

您必须设置upperView.userInteractionEnabled = NO;,否则上方视图将拦截触摸。

接口构建器版本的此选项是“视图属性”面板底部的一个复选框,称为“已启用用户交互”。取消选中它,您应该会很好。


抱歉-应该说。我仍然希望能够与顶部UIView中的对象进行交互。
delany

但是upperView不能接受任何触摸,请在upperView中包含按钮。
imcaptor

2
此解决方案对我有用。我根本不需要俯视来对触摸做出反应。
TJ

11

pointInside:withEvent:的自定义实现确实看起来很可行,但是处理硬编码的坐标对我来说似乎很奇怪。因此,我最终使用CGRectContainsPoint()函数检查了CGPoint是否在按钮CGRect内:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    return (CGRectContainsPoint(disclosureButton.frame, point));
}

8

最近,我写了一个课程,可以帮到我。用它作为用于自定义类UIButtonUIView将通过透明像素已执行的触摸事件。

此解决方案比接受的答案好一些,因为您仍然可以单击UIButton半透明下方的,UIView而的非透明部分UIView仍会响应触摸事件。

GIF

正如您在GIF中看到的那样,“长颈鹿”按钮是一个简单的矩形,但是透明区域上的触摸事件会传递到UIButton下方的黄色。

连结至课程


2
由于代码时间不长,因此您的答案中应包括代码的相关部分,以防将来某个时候移动或删除项目。
加文2015年

谢谢,您的解决方案适用于以下情况:我需要UIView的非透明部分来响应触摸事件,而透明部分则不需要。辉煌!
布鲁斯

@Bruce Glad帮助了您!
塞格夫2015年

4

我想我参加这个聚会有点晚了,但是我将添加以下可能的解决方案:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView != self) return hitView;
    return [self superview];
}

如果使用此代码覆盖自定义UIView的标准hitTest函数,则它将仅忽略视图本身。该视图的所有子视图将正常返回其命中值,而本应进入视图的任何命中结果都将传递到其父视图。

-灰


1
这是我的首选方法,除了,我不认为您应该返回[self superview]。该方法的文档说明“返回包含指定点的视图层次结构中接收者的最远后裔(包括自身)”和“如果该点完全位于接收者视图层次之外,则返回nil”。我认为你应该回来nil。当您返回nil时,控制权将传递给超级视图,以检查是否有任何命中。所以基本上它会做同样的事情,除了返回Superview将来可能会破坏某些东西。
jasongregori 2012年

当然,这可能是明智的(请注意我原始答案的日期-自那时以来进行了很多编码)
Ash

4

只需浏览已接受的答案,然后将其放在此处以供我参考。接受的答案工作完美。您可以像这样扩展它,以允许视图的子视图接收触摸,或将其传递给我们后面的任何视图:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // If one of our subviews wants it, return YES
    for (UIView *subview in self.subviews) {
        CGPoint pointInSubview = [subview convertPoint:point fromView:self];
        if ([subview pointInside:pointInSubview withEvent:event]) {
            return YES;
        }
    }
    // otherwise return NO, as if userInteractionEnabled were NO
    return NO;
}

注意:您甚至不必在子视图树上进行递归,因为每种pointInside:withEvent:方法都可以为您处理。


3

将userInteraction属性设置为禁用可能会有所帮助。例如:

UIView * topView = [[TOPView alloc] initWithFrame:[self bounds]];
[self addSubview:topView];
[topView setUserInteractionEnabled:NO];

(注意:在上面的代码中,“自身”是指视图)

这样,您只能在topView上显示,而不会获得用户输入。所有这些用户触摸都将通过此视图,而底部视图将对此做出响应。我将使用此topView来显示透明图像或对其进行动画处理。


3

这种方法非常干净,并且允许透明子视图也不会对触摸做出反应。只是子类化,UIView并在其实现中添加以下方法:

@implementation PassThroughUIView

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    for (UIView *v in self.subviews) {
        CGPoint localPoint = [v convertPoint:point fromView:self];
        if (v.alpha > 0.01 && ![v isHidden] && v.userInteractionEnabled && [v pointInside:localPoint withEvent:event])
            return YES;
    }
    return NO;
}

@end

2

我的解决方案在这里:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pointInView = [self.toolkitController.toolbar convertPoint:point fromView:self];

    if ([self.toolkitController.toolbar pointInside:pointInView withEvent:event]) {
       self.userInteractionEnabled = YES;
    } else {
       self.userInteractionEnabled = NO;
    }

    return [super hitTest:point withEvent:event];
}

希望这可以帮助


2

您可以执行一些操作来拦截两个视图中的触摸。

顶视图:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
   // Do code in the top view
   [bottomView touchesBegan:touches withEvent:event]; // And pass them on to bottomView
   // You have to implement the code for touchesBegan, touchesEnded, touchesCancelled in top/bottom view.
}

但这就是想法。


这当然是可能的-但需要做很多工作(我认为您必须在底层滚动您自己的触敏对象(例如按钮)?)而且似乎有人不得不在此滚动您自己的触摸对象似乎很奇怪获得看似直观的行为的方式。
delany

我不确定我们是否应该尝试一下。
亚历山德拉·卡萨涅2009年

2

这是Swift版本:

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    return !CGRectContainsPoint(buttonView.frame, point)
}

2

迅捷3

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    for subview in subviews {
        if subview.frame.contains(point) {
            return true
        }
    }
    return false
}

1

我从未使用UI工具包构建过完整的用户界面,因此我对此没有太多经验。这是我认为应该起作用的。

每个UIView以及这个UIWindow都有一个property subviews,它是一个包含所有子视图的NSArray。

您添加到视图的第一个子视图将接收索引0,下一个索引将以此类推。您也可以addSubview:insertSubview: atIndex:insertSubview:aboveSubview:和这样的方法来代替,这些方法可以确定子视图在层次结构中的位置。

因此,请检查您的代码以查看首先添加到UIWindow的视图。那将是0,另一个将是1。
现在,从您的一个子视图到另一个视图,您将执行以下操作:

UIView * theOtherView = [[[self superview] subviews] objectAtIndex: 0];
// or using the properties syntax
UIView * theOtherView = [self.superview.subviews objectAtIndex:0];

让我知道这是否适合您的情况!


(在此标记下方是我先前的答案):

如果视图需要相互通信,则应通过控制器(即使用流行的MVC模型)进行通信。

创建新视图时,可以确保其向控制器注册。

因此,该技术是确保您的视图向控制器注册(该控制器可以按名称或您在Dictionary或Array中喜欢的任何名称存储它们)。您既可以让控制器为您发送消息,也可以获取对该视图的引用并直接与其进行通信。

如果您的视图没有指向控制器的链接(可能是这种情况),则可以使用单例和/或类方法来获取对控制器的引用。


感谢您的答复-但我不确定我是否理解。两个视图都有一个控制器-问题在于,即使没有对象实际“阻止”这些事件,底部视图也不会拾取事件(并将它们转发到其控制器)。顶视图。
delany

您的UIViews顶部是否有一个UIWindow?如果这样做,则应该传播事件,而无需做任何“魔术”。在Apple开发人员中心阅读有关[Window and Views] [1]的信息(如果不能解决问题,请务必添加另一条评论!)[1]:developer.apple.com/iphone/library/documentation/ iPhone /…
纳什

是的,绝对是-层次结构顶部的UIWindow。
delany

我已经更新了答案,以包含用于遍历视图层次结构的代码。让我知道您是否需要其他帮助!
nash

感谢您的帮助nash-但我在构建这些接口方面经验丰富,据我所知,我目前的接口已正确设置。将全视图置于其他视图之上时,问题似乎是其默认行为。
delany

1

我认为正确的方法是使用构建到视图层次结构中的视图链。对于推送到主视图的子视图,请勿使用通用的UIView,而应使用UIView的子类(或其子类之一,如UIImageView)来制成MYView:UIView(或所需的任何超类型,如UIImageView)。在YourView的实现中,实现touchesBegan方法。然后在触摸该视图时将调用此方法。在该实现中,您所需要的只是一个实例方法:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event ;
{   // cannot handle this event. pass off to super
    [self.superview touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event]; }

这个touchesBegan是响应者api,因此您无需在公共或私有接口中声明它;它是您仅需了解的那些魔术API之一。这个self.superview将最终使请求冒泡到viewController。然后,在viewController中,实现touchesBegan以处理触摸。

请注意,触摸位置(CGPoint)会随着视图层次结构链中的反弹而自动为您调整。


1

只是想发布此内容,因为我遇到了一些类似的问题,花了大量时间试图在这里实现答案而没有任何运气。我最终要做的是:

 for(UIGestureRecognizer *recognizer in topView.gestureRecognizers)
 {
     recognizer.delegate=self;
     [bottomView addGestureRecognizer:recognizer];   
 }
 topView.abView.userInteractionEnabled=NO; 

并实施UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

底视图是一个导航控制器,带有许多命令,我在其顶部有一扇门,可以用平移手势将其关闭。整个内容都嵌入了另一个VC。像魅力一样工作。希望这可以帮助。


1

基于HitTest的解决方案的Swift 4实现

let hitView = super.hitTest(point, with: event)
if hitView == self { return nil }
return hitView

0

源自Stuart出色的,几乎是万无一失的答案以及Segev的有用实现,这是一个Swift 4软件包,您可以将其放入任何项目中:

extension UIColor {
    static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {

        var pixel: [CUnsignedChar] = [0, 0, 0, 0]

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

        let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)

        context!.translateBy(x: -point.x, y: -point.y)

        view.layer.render(in: context!)

        let red: CGFloat   = CGFloat(pixel[0]) / 255.0
        let green: CGFloat = CGFloat(pixel[1]) / 255.0
        let blue: CGFloat  = CGFloat(pixel[2]) / 255.0
        let alpha: CGFloat = CGFloat(pixel[3]) / 255.0

        let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)

        return color
    }
}

然后用hitTest:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
    return super.hitTest(point, with: event)
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.