iOS - 将所有触摸事件转发到一个视图上

171

我有一个视图在许多其他视图的上方重叠。我仅使用这个叠加视图来检测屏幕上的一些点击,但除此之外,我不希望该视图停止底部的其他视图(例如滚动视图等)的行为。如何将所有触摸事件转发到这个叠加视图下面的其他视图?该叠加视图是UIView的子类。


3
你解决了你的问题吗?如果是,那么请接受答案,这样其他人就可以更容易地找到解决方案,因为我也遇到了同样的问题。 - Khushbu Desai
这个答案很好用 https://dev59.com/mnA75IYBdhLWcg3w49YR#4010809 - Lance Samaria
如果您的问题是视图位于UINavigationBar后面(您可以通过使用XCode的3D视图堆栈调试器在调试部分(三个小方块堆叠的图标)中观察到),那么子类化UIView以使其通过穿透可能不足够(甚至不是真正的问题!)。但我偶然发现了这个答案,它作为扩展覆盖了UINavigationBar中的方法,并且对于导航栏阻止触摸问题非常有效。https://dev59.com/gmEh5IYBdhLWcg3w9nhj#61312628 - clearlight
13个回答

143

禁用用户交互就是我所需要的!

Objective-C:

myWebView.userInteractionEnabled = NO;

Swift:

myWebView.isUserInteractionEnabled = false

1
这在我的情况下起作用,我有一个按钮的子视图。我禁用了子视图上的交互,然后按钮就可以工作了。 - simple_code
3
在Swift 4中,myWebView.isUserInteractionEnabled = false的意思是禁用名为myWebView的WebView视图的用户交互功能。 - Louis 'LYRO' Dupont
1
请注意,使用此解决方案还会删除Voice Over读取元素的任何可访问性标签的能力,例如标签。 - Wez

134

为了使覆盖视图下方的视图接收到触摸事件,需要在UIView中实现以下方法:

Objective-C:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"Passing all touches to the next view (if any), in the view stack.");
    return NO;
}

Swift 5:

->

Swift 5:

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    print("Passing all touches to the next view (if any), in the view stack.")
    return false
}

5
我遇到了同样的问题,但我想要的是如果我使用手势放大/旋转/移动视图,则应返回“是”,否则返回“否”,我该如何实现这一点? - Minakshi
1
@Minakshi,那是完全不同的问题,请为此提出一个新问题。 - Fattie
1
但是请注意,这种简单的方法无法让您在所讨论的图层上方拥有按钮等控件! - Fattie
1
@Minakshi 你好,你找到你的问题的答案了吗? - Lance Samaria

65

这是一篇旧的帖子,但它在搜索中出现了,所以我想加入我的意见。我有一个带有子视图的UIView,并且 只有希望拦截击中其中一个子视图的触摸事件,因此我修改了PixelCloudSt的答案:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    for (UIView* subview in self.subviews ) {
        if ( [subview hitTest:[self convertPoint:point toView:subview] withEvent:event] != nil ) {
            return YES;
        }
    }
    return NO;
}

1
你需要创建一个自定义的UIView子类(或任何其他UIView子类的子类,例如UIImageView或其他),并覆盖此函数。实质上,子类可以在.h文件中具有完全空的'@interface',而上述函数则是'@implementation'的全部内容。 - fresidue
1
谢谢,这正是我需要的,可以通过我的UIView的空白区域传递触摸事件,由后面的视图处理。+100 - Danny
我只是想再次点赞这个帖子,大概已经有2年了....... - bojan
这正是我在寻找的。在转换后,它也在 Swift 5.6 中运行得很好! - erik_m_martens

59

这是 @fresidue 答案的改进版本。你可以使用这个 UIView 子类作为透明视图,传递触摸事件到其子视图之外。以下是 Objective-C 实现代码:

@interface PassthroughView : UIView

@end

@implementation PassthroughView

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    for (UIView *view in self.subviews) {
        if (!view.hidden && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
            return YES;
        }
    }
    return NO;
}

@end

在Swift中:

class PassthroughView: UIView {
  override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    return subviews.contains(where: {
      !$0.isHidden
      && $0.isUserInteractionEnabled
      && $0.point(inside: self.convert(point, to: $0), with: event)
    })
  }
}

TIP:

假设您有一个很大的“容器”面板,可能带有表视图。您可以将“容器”面板设置为PassthroughView。现在它可以正常工作了,您可以通过“容器”面板滚动表格。

但是!

  1. 在“容器”面板上方放置一些标签或图标。当然,别忘了必须将这些标签或图标的用户交互启用关闭!

  2. 在“容器”面板顶部添加了一些按钮。当然,别忘了必须将这些按钮的用户交互启用开启!

  3. 请注意:有点令人困惑的是,您使用PassthroughView的“容器”本身必须启用用户交互开启! 是开启!(否则,PassthroughView中的代码将永远不会被调用。)


1
我已经使用了Swift提供的解决方案。它在iOS 11上运行良好,但在iOS 10上每次都会崩溃。显示Bad Access。有什么想法吗? - Soumen
你救了我的一天,它成功地传递了触摸到下面的UIView。 - AaoIi
1
不要忘记检查 $0.isUserInteractionEnabled - Sean Thielen

12

我需要通过一个UIStackView传递触摸事件。其中一个UIView是透明的,但UIStackView会消耗所有的触摸事件。以下方法对我有用:

class PassThrouStackView: UIStackView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        if view == self {
            return nil
        }
        return view
    }
}

所有arrangedSubviews仍然接收触摸事件,但是对UIStackView本身的触摸事件会穿透到下面的视图(对我来说是地图视图)。


只是想补充一下,您也可以通过扩展UIView来应用此方法。以下是一篇文章,使用完全相同的方法并进一步探讨了其他用例: https://medium.com/@nguyenminhphuc/how-to-pass-ui-events-through-views-in-ios-c1be9ab1626b - Kacy
UIScrollView是否可以实现? - Midhun Narayan

8

我遇到了一个与UIStackView类似的问题(但可能也适用于其他视图)。

我的配置如下:

包含containerView和StackView的视图控制器

在这个经典案例中,我需要将一个容器置于背景中,并在侧边添加按钮。出于布局目的,我将按钮包含在UIStackView中,但现在stackView的中间(空)部分拦截了触摸操作 :-(

我创建了一个UIStackView的子类,其中包含一个属性,定义了应该响应触摸操作的子视图。现在,任何对“viewsWithActiveTouch”数组中的侧边按钮的触摸操作都会传递到按钮上,而在除了这些视图之外的stackview任何其他地方的触摸操作都不会被拦截,因此会传递到位于stack view下面的任何内容。

/** Subclass of UIStackView that does not accept touches, except for specific subviews given in the viewsWithActiveTouch array */
class NoTouchStackView: UIStackView {

  var viewsWithActiveTouch: [UIView]?

  override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    if let activeViews = viewsWithActiveTouch {
        for view in activeViews {
           if CGRectContainsPoint(view.frame, point) {
                return view

           }
        }
     }
     return nil
   }
}

6

如果您希望将触摸事件转发到的视图不是子视图/父视图,您可以在UIView子类中设置自定义属性,如下所示:

@interface SomeViewSubclass : UIView {

    id forwardableTouchee;

}
@property (retain) id forwardableTouchee;

请确保在您的 .m 文件中进行综合处理:
@synthesize forwardableTouchee;

然后在任何UIResponder方法中包含以下内容,例如:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    [self.forwardableTouchee touchesBegan:touches withEvent:event];

}

无论在何处实例化您的UIView,都将forwardableTouchee属性设置为您想要将事件转发到的视图:

    SomeViewSubclass *view = [[[SomeViewSubclass alloc] initWithFrame:someRect] autorelease];
    view.forwardableTouchee = someOtherView;

5

在Swift 5中

class ThroughView: UIView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        guard let slideView = subviews.first else {
            return false
        }

        return slideView.hitTest(convert(point, to: slideView), with: event) != nil
    }
}

4

看起来虽然这里有很多答案,但没有一个干净的Swift解决我需要的问题。因此,我采用了@fresidue的答案,并将其转换为Swift,因为现在大多数开发人员都想在这里使用它。

它解决了我的问题,其中我有一些带有按钮的透明工具栏,但我希望工具栏对用户不可见,并且触摸事件应该穿过工具栏。

根据我的测试,isUserInteractionEnabled = false不是一个选项。

 override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    for subview in subviews {
        if subview.hitTest(convert(point, to: subview), with: event) != nil {
            return true
        }
    }
    return false
}

这也是我的经验。因此,我正在测试一个点的方法。实际上,苹果文档中对于.isUserInteractionEnabled表明,如果为false,则视图目标事件将从事件视图中移除,并且显然对于视图的触摸是针对该视图的。 - clearlight

2

我在StackView中有几个标签,但是我并没有成功地使用上面的解决方案,相反,我使用了下面的代码来解决我的问题:

let item = ResponsiveLabel()

// Configure label

stackView.addArrangedSubview(item)

UIStackView的子类化:

class PassThrouStackView:UIStackView{
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        for subview in self.arrangedSubviews {
            let convertedPoint = convert(point, to: subview)
            let labelPoint = subview.point(inside: convertedPoint, with: event)
            if (labelPoint){
                return subview
            }
            
        }
        return nil
    }
    
}

然后你可以这样做:

class ResponsiveLabel:UILabel{
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // Respond to touch
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接