写点什么

通过 UIView 和 UIControl 实现的蒙层,哪种更简单?

用户头像
关注
发布于: 3 小时前
通过 UIView 和 UIControl 实现的蒙层,哪种更简单?

在 APP 内,经常需要弹出一个半屏的 UIView 来提示更多信息,比如在底部弹出的分享渠道选项,在顶部弹出的列表筛选选项,在中间弹出的广告消息。


比如点击微信公众号右上角之后,弹出的信息页面:



在显示包含信息的 View 时,通常需要在下面添加一个蒙层,来将弹出的 View 和当前正在显示的内容进行隔离。当不需要用户强制选择某个选项时,点击蒙层也充当了取消的功能。


UIView 实现的蒙层


可以通过 UIView + 手势识别来实现的这个蒙层:


1. 自定义一个 UIView,设置一个带透明度的颜色值;

2. 给这个 View 添加点击手势,实现这个手势的代理;

3. 在手势代理中判断手势点击的坐标,如果包含在显示的内容区域,则不处理,否则执行隐藏方法。


另外,UIView 本身可以处理点击事件,所以可以在 `touchesBegan` 中判断触摸的位置,省去了添加手势的过程:

override func touchesBegan(_ touches: Set<UITouch>, 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 实现的蒙层

最近在阅读 SwiftMessages 源码时,看到使用 UIControl 实现的蒙层 PassthroughView

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 时,只需要设置相应的蒙层颜色,添加点击回调的 tappedHander 就可以了:

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 使用的 addTarget(_:action:for:),UIView 通过添加点击手势或者使用 touchesBegan 来获取点击事件;

3. 过滤点击事件:UIControl 不需要过滤点击事件(上面的 PassthroughViewhitTest 方法中,只是通过判断 tappedHander 是否为空,决定 self 是否响应该事件),UIView 需要在手势回调或者在 touchesBegan 中过滤掉点击在子 view 上的事件。

在阅读源码的过程中,总是能惊奇的发现,还有更多/更好的方案。


其他

根据 UIControl 的实现思路,我尝试能不能更加简化代码,直接在 hitTest 中移除蒙层:


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,但是苹果应该会尽量避免这个吧

用户头像

关注

还未添加个人签名 2017.10.17 加入

还未添加个人简介

评论

发布
暂无评论
写错上百遍之后,才知道蒙层应该这样写