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

问题描述 投票:115回答:19

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

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

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

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

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

objective-c ios cocoa-touch
19个回答
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:方法。


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.
}

但那是个主意。


2
投票

这是一个Swift版本:

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

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
}

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和/或类方法来获取对控制器的引用。


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

底部视图是一个带有多个segues的导航控制器,我在它上面有一个门,可以用平移手势关闭。整件事嵌入了另一个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)
}

40
投票

虽然这里的许多答案都有效,但我有点惊讶地发现这里没有给出最方便,最通用和最简单的答案。 @Ash最接近,除了返回超级视图有一些奇怪的事情......不要这样做。

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

这个解决方案是:

  • 方便,因为它不需要引用任何其他视图/子视图/对象;
  • 泛型,因为它适用于任何纯粹作为可触摸子视图的容器的视图,并且子视图的配置不会影响其工作方式(如果您覆盖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设置为YES一样简单。


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];
}

你也可以像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


29
投票

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

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


11
投票

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

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

8
投票

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

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

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

Link to class


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

-灰


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

(注意:在上面的代码中,'self'指的是一个视图)

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

© www.soinside.com 2019 - 2024. All rights reserved.