使用hitTest()或点(在内部,带有事件)从视图转发事件,将视图添加到keyWindow

时间:2019-02-17 00:10:56

标签: ios swift gesture uiwindow

我有一个小型的,可重复使用的UIView小部件,可以将其添加到任何地方的任何视图中,并且可能或不一定总是在同一位置或具有相同的框架。看起来像这样:

class WidgetView: UIView {
    // some stuff, not very exciting
}

在我的窗口小部件视图中,有一种情况需要创建一个弹出菜单,该弹出菜单下有一个叠加层。看起来像这样:

class WidgetView: UIView {
    // some stuff, not very exciting

    var overlay: UIView!

    commonInit() {
        guard let keyWindow = UIApplication.shared.keyWindow else { return }
        overlay = UIView(frame: keyWindow.frame)
        overlay.alpha = 0
        keyWindow.addSubview(overlay)

        // Set some constraints here

        someControls = CustomControlsView( ... a smaller controls view ... ) 
        overlay.addSubview(someControls)

        // Set some more constraints here!
    }            

    showOverlay() {
        overlay.alpha = 1
    }

    hideOverlay() {
        overlay.alpha = 0
    }
}

这变得很复杂的地方是,我要从叠加层中裁剪出原始WidgetView的形状,以便其控件仍在下面可见。效果很好:

class CutoutView: UIView {

    var holes: [CGRect]?

    convenience init(holes: [CGRect], backgroundColor: UIColor?) {
        self.init()

        self.holes = holes

        self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.5)
        isOpaque = false
    }

    override func draw(_ rect: CGRect) {
        backgroundColor?.setFill()
        UIRectFill(rect)

        guard let rectsArray = holes else {
            return
        }

        for holeRect in rectsArray {
            let holeRectIntersection = rect.intersection(holeRect)
            UIColor.clear.setFill()
            UIRectFill(holeRectIntersection)
        }
    }
}

...除了 问题

触摸不会穿过切口孔。因此,我认为我会很聪明,并使用this扩展名来确定触摸点上的像素是否透明,但是我什至无法做到这一点,因为hitTest()和{{ 1}}对point(inside, with event)框架之外的触摸不做出反应。

我看到的方式有四种(潜在的)解决方法,但是我无法使它们中的任何一种起作用。

  1. 找到某种神奇的()方法来使hitTest或point(inside)在WidgetView或至少keyWindow的帧中的任何地方进行响应

  2. overlayView添加UITapGestureRecognizer并将适当的触摸转发到原始视图控制器(这部分起作用-轻击手势会响应,但是我不知道从哪里去在那里)

  3. 使用委托/协议实现告诉原始overlayView响应触摸

  4. 将叠加层及其子视图添加到不是keyWindow的其他父视图中吗?


下面,这是一个完整的可执行设置,该设置依赖于带有故事板的新单视图项目。它依靠SnapKit约束,您可以使用以下podfile:

podfile

WidgetView

ViewController.swift

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target 'YourTarget' do
    pod 'SnapKit', '~> 4.2.0'
end

CutoutView.swift

import UIKit
import SnapKit

class ViewController: UIViewController {

    public var utilityToolbar: UtilityToolbar!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .darkGray
        setup()

    }

    func setup() {

        let button1 = UtilityToolbar.Button(title: "One", buttonPressed: nil)
        let button2 = UtilityToolbar.Button(title: "Two", buttonPressed: nil)
        let button3 = UtilityToolbar.Button(title: "Three", buttonPressed: nil)
        let button4 = UtilityToolbar.Button(title: "Four", buttonPressed: nil)
        let button5 = UtilityToolbar.Button(title: "Five", buttonPressed: nil)

        let menuItems: [UtilityToolbar.Button] = [button1, button2, button3, button4, button5]
        menuItems.forEach({
            $0.setTitleColor(#colorLiteral(red: 0.1963312924, green: 0.2092989385, blue: 0.2291107476, alpha: 1), for: .normal)
        })

        utilityToolbar = UtilityToolbar(title: "One", menuItems: menuItems)
        utilityToolbar.titleButton.setTitleColor(#colorLiteral(red: 0.1963312924, green: 0.2092989385, blue: 0.2291107476, alpha: 1), for: .normal)
        utilityToolbar.backgroundColor = .white
        utilityToolbar.dropdownContainer.backgroundColor = .white

        view.addSubview(utilityToolbar)

        utilityToolbar.snp.makeConstraints { (make) in
            make.left.right.equalToSuperview()
            make.top.equalToSuperview().offset(250)
            make.height.equalTo(50.0)
        }
    }
}

UtilityToolbar.swift

import UIKit

class CutoutView: UIView {

    var holes: [CGRect]?

    convenience init(holes: [CGRect], backgroundColor: UIColor?) {
        self.init()
        self.holes = holes
        self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.5)
        isOpaque = false
    }

    override func draw(_ rect: CGRect) {
        backgroundColor?.setFill()
        UIRectFill(rect)

        guard let rectsArray = holes else { return }

        for holeRect in rectsArray {
            let holeRectIntersection = rect.intersection(holeRect)
            UIColor.clear.setFill()
            UIRectFill(holeRectIntersection)
        }
    }

}

Extensions.swift

import Foundation import UIKit import SnapKit

class UtilityToolbar: UIView {

    class Button: UIButton {

        var functionIdentifier: String?
        var buttonPressed: (() -> Void)?

        fileprivate var owner: UtilityToolbar?

        convenience init(title: String, buttonPressed: (() -> Void)?) {
            self.init(type: .custom)
            self.setTitle(title, for: .normal)
            self.functionIdentifier = title.lowercased()
            self.buttonPressed = buttonPressed
        }
    }

    enum MenuState {
        case open
        case closed
    }

    enum TitleStyle {
        case label
        case dropdown
    }

    private(set) public var menuState: MenuState = .closed

    var itemHeight: CGFloat = 50.0
    var spacing: CGFloat = 6.0 { didSet { dropdownStackView.spacing = spacing } }
    var duration: TimeInterval = 0.15
    var dropdownContainer: UIView!
    var titleButton: UIButton = UIButton()

    @IBOutlet weak fileprivate var toolbarStackView: UIStackView!
    private var stackViewBottomConstraint: Constraint!
    private var dropdownStackView: UIStackView!
    private var overlayView: CutoutView!
    private var menuItems: [Button] = []
    private var expandedHeight: CGFloat { get { return CGFloat(menuItems.count - 1) * itemHeight + (spacing * 2) } }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    convenience init(title: String, menuItems: [Button]) {
        self.init()
        self.titleButton.setTitle(title, for: .normal)
        self.menuItems = menuItems
        commonInit()
    }

    private func commonInit() {

        self.addSubview(titleButton)
        titleButton.addTarget(self, action: #selector(titleButtonPressed(_:)), for: .touchUpInside)
        titleButton.snp.makeConstraints { $0.edges.equalToSuperview() }

        dropdownContainer = UIView()

        dropdownStackView = UIStackView()
        dropdownStackView.axis = .vertical
        dropdownStackView.distribution = .fillEqually
        dropdownStackView.alignment = .fill
        dropdownStackView.spacing = spacing
        dropdownStackView.alpha = 0
        dropdownStackView.translatesAutoresizingMaskIntoConstraints = true

        menuItems.forEach({
            $0.owner = self
            $0.addTarget(self, action: #selector(menuButtonPressed(_:)), for: .touchUpInside)
        })
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // Block if the view isn't fully ready, or if the containerView has already been added to the window
        guard
            let keyWindow = UIApplication.shared.keyWindow,
            self.globalFrame != .zero,
            dropdownContainer.superview == nil else { return }

        overlayView = CutoutView(frame: keyWindow.frame)
        overlayView.backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.5)
        overlayView.alpha = 0
        overlayView.holes = [self.globalFrame!]
        keyWindow.addSubview(overlayView)
        keyWindow.addSubview(dropdownContainer)
        dropdownContainer.snp.makeConstraints { (make) in
            make.left.right.equalToSuperview()
            make.top.equalToSuperview().offset((self.globalFrame?.origin.y ?? 0) + self.frame.height)
            make.height.equalTo(0)
        }

        dropdownContainer.addSubview(dropdownStackView)

        dropdownStackView.snp.makeConstraints({ (make) in
            make.left.right.equalToSuperview().inset(spacing).priority(.required)
            make.top.equalToSuperview().priority(.medium)
            stackViewBottomConstraint = make.bottom.equalToSuperview().priority(.medium).constraint
        })
    }

    public func openMenu() {

        titleButton.isSelected = true
        dropdownStackView.addArrangedSubviews(menuItems.filter { $0.titleLabel?.text != titleButton.titleLabel?.text })
        dropdownContainer.layoutIfNeeded()
        dropdownContainer.snp.updateConstraints { (make) in
            make.height.equalTo(self.expandedHeight)
        }

        stackViewBottomConstraint.update(inset: spacing)

        UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
            self.overlayView.alpha = 1
            self.dropdownStackView.alpha = 1
            self.dropdownContainer.superview?.layoutIfNeeded()
        }) { (done) in
            self.menuState = .open
        }

    }

    public func closeMenu() {

        titleButton.isSelected = false
        dropdownContainer.snp.updateConstraints { (make) in
            make.height.equalTo(0)
        }
        stackViewBottomConstraint.update(inset: 0)

        UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
            self.overlayView.alpha = 0
            self.dropdownStackView.alpha = 0
            self.dropdownContainer.superview?.layoutIfNeeded()
        }) { (done) in
            self.menuState = .closed
            self.dropdownStackView.removeAllArrangedSubviews()
        }
    }

    @objc private func titleButtonPressed(_ sender: Button) {
        switch menuState {
        case .open:
            closeMenu()
        case .closed:
            openMenu()
        }
    }

    @objc private func menuButtonPressed(_ sender: Button) {
        closeMenu()
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // Nothing of interest is happening here unless the touch is inside the containerView
        print(UIColor.colorOfPoint(point: point, in: overlayView).cgColor.alpha > 0)
        if UIColor.colorOfPoint(point: point, in: overlayView).cgColor.alpha > 0 {
            return true
        }
        return super.point(inside: point, with: event)
    } }

1 个答案:

答案 0 :(得分:1)

对不起,我需要将hitTest放在叠加视图(CutoutView)上而不是调用视图上。

class CutoutView: UIView {

    // ...

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
        return super.hitTest(point, with: event)
    }
}