通过 UIView 和 UIControl 实现的蒙层,哪种更简单?
在 APP 内,经常需要弹出一个半屏的 UIView 来提示更多信息,比如在底部弹出的分享渠道选项,在顶部弹出的列表筛选选项,在中间弹出的广告消息。
比如点击微信公众号右上角之后,弹出的信息页面:

在显示包含信息的 View 时,通常需要在下面添加一个蒙层,来将弹出的 View 和当前正在显示的内容进行隔离。当不需要用户强制选择某个选项时,点击蒙层也充当了取消的功能。
UIView 实现的蒙层
可以通过 UIView + 手势识别来实现的这个蒙层:
1. 自定义一个 UIView,设置一个带透明度的颜色值;
2. 给这个 View 添加点击手势,实现这个手势的代理;
3. 在手势代理中判断手势点击的坐标,如果包含在显示的内容区域,则不处理,否则执行隐藏方法。
另外,UIView 本身可以处理点击事件,所以可以在 `touchesBegan` 中判断触摸的位置,省去了添加手势的过程:
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
// 查看点击事件是否落在了某个子 view 上
let subview = subviews.first {
touches.first?.view?.isDescendant(of: $0) ?? false
}
if subview == nil {
// 点击事件没有落在任何子 view 上,则执行隐藏/取消事件
}
}
判断某个位置是否在某个视图上有多种方式,可以查看:[iOS判断当前点击的位置是否在某个视图上的几种方法](https://www.jianshu.com/p/f4e29487b4cd)
UIControl 实现的蒙层
最近在阅读 源码时,看到使用 UIControl 实现的蒙层 :
class PassthroughView: UIControl {
var tappedHander: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
initCommon()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initCommon()
}
private func initCommon() {
addTarget(self, action: #selector(tapped), for: .touchUpInside)
}
@objc func tapped() {
tappedHander?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self && tappedHander == nil ? nil : view
}
}
使用 UIControl 实现的蒙层 PassthroughView 时,只需要设置相应的蒙层颜色,添加点击回调的 就可以了:
let passthroughView: PassthroughView = {
let view = PassthroughView(frame: self.view.bounds)
view.backgroundColor = .lightGray
view.tappedHander = { [weak view] in
view?.removeFromSuperview()
}
return view
}()
小结
我们将 UIControl 和 UIView 的实现过程进行对比:
1. 基类不同:一个 UIControl, 一个 UIView。UIControl 继承自 UIView,UIView 比较轻量级;
2. 添加点击事件:UIControl 使用的 ,UIView 通过添加点击手势或者使用 来获取点击事件;
3. 过滤点击事件:UIControl 不需要过滤点击事件(上面的 的 方法中,只是通过判断 tappedHander 是否为空,决定 self 是否响应该事件),UIView 需要在手势回调或者在 中过滤掉点击在子 view 上的事件。
在阅读源码的过程中,总是能惊奇的发现,还有更多/更好的方案。
其他
根据 UIControl 的实现思路,我尝试能不能更加简化代码,直接在 中移除蒙层:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self {
// 如果点击在蒙层上,则直接将 self 移除
self.removeFromSuperview()
}
return view
}
但是发现这样是不行的。至于为什么不行,我请教了一位朋友,觉得他说的挺有道理的:
我的理解是:事件处理的优先级是最高的,并且程序一启动就有一个 RunLoop 去处理 App 的事件队列,当发现有事件需要处理,就开始处理这个事件,而这个事件是主线程的,移除的话也是主线程,所以这里应该会有问题
还有一个问题就是,这个方法是返回一个最合适的 view 去响应事件,你要是把 View 给移除了,如果没有内存泄漏,那么就释放了这个 View,返回的那个 View 就不存在了,不知道是否会存在什么问题
虽然说给 nil 对象发送消息,不会crash,但是苹果应该会尽量避免这个吧