Swift 简单实现<可容纳多个浮窗视图的容器>

平时开发经常有一些视图会以浮窗形式呈现,但是如果这些浮窗都放在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的子类(包括私有类)都可以拥有穿透性,但这是全局性的底层修改,不太安全,得斟酌使用。

以上两种方式各有各好处,看情况使用吧。

That is all, thank you.

你可能感兴趣的:(Swift 简单实现<可容纳多个浮窗视图的容器>)