允许在另一个UIView下与UIView进行交互

时间:2009-11-07 21:43:41

标签: objective-c ios cocoa-touch

是否有一种简单的方法允许与位于另一个UIView下的UIView中的按钮进行交互 - 在按钮顶部没有来自顶部UIView的实际对象?

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

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

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

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

19 个答案:

答案 0 :(得分: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:方法。

答案 1 :(得分:40)

虽然这里的许多答案都有效,但我有点惊讶地发现这里没有给出最方便,最通用和最简单的答案。

这个答案取自我对类似问题的回答here

- (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,指定此视图应接收触摸。响应者链的工作方式意味着将继续遍历此点上方的视图层次结构,直到找到将响应触摸的视图。 不要返回超级视图,因为它的高级视图是否应该接受触摸不符合此视图!

此解决方案是:

  • 方便,因为它不需要引用任何其他视图/子视图/对象;
  • 泛型,因为它适用于纯粹作为可触摸子视图的容器的任何视图,并且子视图的配置不会影响其工作方式(如果您覆盖{{ 1}}返回特定的可触摸区域。)
  • 万无一失,代码不多......而且这个概念并不难理解。

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

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设置为onlyRespondToTouchesInSubviews一样简单。

答案 2 :(得分:30)

有几种方法可以解决这个问题。我最喜欢的是覆盖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];
}

您也可以像gyim建议的那样修改pointInside:withEvent:方法。这样可以通过在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

答案 3 :(得分:29)

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

Interface Builder版本是View Attributes面板底部的一个复选框,名为“User Interaction Enabled”。取消选中它,你应该好好去。

答案 4 :(得分:11)

pointInside的自定义实现:withEvent:确实看起来像是要走的路,但处理硬编码坐标对我来说似乎很奇怪。所以我最后使用CGRectContainsPoint()函数检查CGPoint是否在CGRect按钮内:

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

答案 5 :(得分:8)

最近我写了一堂课,帮助我解决这个问题。将其用作UIButtonUIView的自定义类会传递在透明像素上执行的触摸事件。

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

GIF

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

Link to class

答案 6 :(得分: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函数,它将仅忽略视图本身。该视图的任何子视图都会正常返回它们的命中,并且任何已经转到视图本身的命中都会被传递给它的超级视图。

-Ash

答案 7 :(得分: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:方法都会为您处理。

答案 8 :(得分:3)

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

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

(注意:在上面的代码中,&#39; self&#39;指视图)

这样,您只能在topView上显示,但不会获得用户输入。所有这些用户触摸都将通过此视图,底部视图将响应它们。我使用这个topView来显示透明图像,或者为它们制作动画。

答案 9 :(得分: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

答案 10 :(得分: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.
}

但这就是主意。

答案 11 :(得分: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];
}

希望这有帮助

答案 12 :(得分:2)

Swift 3

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

答案 13 :(得分:2)

这是一个Swift版本:

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

答案 14 :(得分:1)

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

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

您添加到视图的第一个子视图将接收索引0,下一个索引1将依此类推。您还可以将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模型)进行通信。

创建新视图时,可以确保它使用控制器注册自己。

因此,该技术是确保您的视图向控制器注册(控制器可以按名称或您喜欢的字典或数组存储它们)。您可以让控制器为您发送消息,也可以直接与视图进行通信。

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

答案 15 :(得分:1)

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

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

答案 16 :(得分: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;
}

底部视图是一个带有多个segues的导航控制器,我的顶部有一扇门,可以用平移手势关闭。整件事嵌入了另一个VC。工作就像一个魅力。希望这会有所帮助。

答案 17 :(得分: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)会相对于包围视图自动调整,因为它会在视图层次结构链中反弹。

答案 18 :(得分: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)
}