平时开发经常有一些视图会以浮窗形式呈现,但是如果这些浮窗都放在ViewController的view上,就会很难管理,所以最好使用专门的一个容器来管理这些浮窗。
实现浮窗视图的容器,无非就是当手指触碰到浮窗视图就拦截手势事件,不再传递;而没碰到浮窗视图(触碰容器本身)的话,手势事件就直接穿透到下一层(不响应)。
Core code
既然是触碰相关的修改,那就是重写UIView的hitTest
方法:
class PenetrableContainer: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !isHidden, alpha > 0.01, subviews.count > 0 else {
// 自身不响应
return nil
}
// 子视图从【顶层】开始遍历
for subview in subviews.reversed() {
// 判断一个`View`是否能响应的条件:
guard subview.isUserInteractionEnabled, // 1.能否交互
!subview.isHidden, // 2.非隐藏
subview.alpha > 0.01, // 3.非透明
subview.frame.contains(point) // 4.触碰点是否属于视图区域内
else { continue }
// 转换为相对于子视图上的触碰点
let subPoint = convert(point, to: subview)
guard let rspView = subview.hitTest(subPoint, with: event) else { continue }
return rspView
}
// 自身不响应
return nil
}
}
只响应子视图而自身不响应的浮窗容器,就这样实现了。
可以直接设置浮窗容器为整个屏幕的大小,然后随意往上面丢浮窗。不用再担心手势被拦截的问题,同时也方便管理各种浮窗。
使用效果
1. 多个小浮窗共用一个容器:
- 手指触碰到浮窗以外的区域都不会被拦截。
2. 多个浮窗容器重叠:
- 多个浮窗容器重叠也互不影响。
可以的,简单粗暴,满足需求。
使用扩展
1. 继承
自定义View,直接继承上面的PenetrableContainer
:
class MyView: PenetrableContainer { ... }
- 可以让任意自定义View继承其穿透性,但是非UIView子类(如UITableView)就无法继承,只能针对性新建其子类去重写
hitTest
方法。
2. 方法交换
创建UIView的扩展,使用Runtime交换hitTest
方法的实现,并且创建一个关联对象(分类属性)来控制是否可穿透:
import UIKit
private var _isPenetrate: CChar = 0
extension UIView {
// 单例方法:交换`hitTest`方法
static let penetrateHook: Void = { swizzlingHitTest() }()
// 是否可穿透
var isPenetrate: Bool {
set { objc_setAssociatedObject(self, &_isPenetrate, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
get { objc_getAssociatedObject(self, &_isPenetrate) as? Bool ?? false }
}
private static func swizzlingHitTest() {
guard let originalMethod = class_getInstanceMethod(self, #selector(hitTest(_:with:))),
let swizzledMethod = class_getInstanceMethod(self, #selector(penetrate_hitTest(_:with:))) else {
return
}
method_exchangeImplementations(originalMethod, swizzledMethod)
}
@objc private func penetrate_hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard isPenetrate else {
// 不可穿透:自己调用`penetrate_hitTest` -> 执行【原方法】hitTest
return penetrate_hitTest(point, with: event)
}
// 可穿透:拦截点击 => 自己不响应,触碰的子视图响应。
guard !isHidden, alpha > 0.01, subviews.count > 0 else { return nil }
for subview in subviews.reversed() where subview.isUserInteractionEnabled && !subview.isHidden && subview.alpha > 0.01 && subview.frame.contains(point) {
let subPoint = convert(point, to: subview)
// 其他对象调用`hitTest` -> 执行【交换后的方法】penetrate_hitTest
guard let rspView = subview.hitTest(subPoint, with: event) else { continue }
return rspView
}
return nil
}
}
由于Swift不能重写UIView的+load方法,所以得在didFinishLaunchingWithOptions
调用一下交换hitTest
方法的单例:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 交换方法
UIView.penetrateHook
return true
}
只要想有穿透性,设置isPenetrate = true
就行了:
let myView = UIView()
myView.isPenetrate = true
let stackView = UIStackView()
stackView.isPenetrate = true
- 这种方式更加灵活,只要是UIView的子类(包括私有类)都可以拥有穿透性,但这是全局性的底层修改,不太安全,得斟酌使用。
以上两种方式各有各好处,看情况使用吧。