平时开发常常有一些视图会以浮窗形式出现,可是如果这些浮窗都放在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的子类(包括私有类)都能够拥有穿透性,但这是全局性的底层修改,不太安全,得斟酌运用。
以上两种方法各有各好处,看情况运用吧。