平时开发常常有一些视图会以浮窗形式出现,可是如果这些浮窗都放在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. 多个小浮窗共用一个容器:

Swift 简单实现

Swift 简单实现

  • 手指触碰到浮窗以外的区域都不会被阻拦。

2. 多个浮窗容器重叠:

Swift 简单实现

Swift 简单实现

  • 多个浮窗容器重叠也互不影响。

能够的,简单粗暴,满足需求。

运用扩展

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.