我有一个小型的,可重复使用的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)
框架之外的触摸不做出反应。
我看到的方式有四种(潜在的)解决方法,但是我无法使它们中的任何一种起作用。
找到某种神奇的()方法来使hitTest或point(inside)在WidgetView
或至少keyWindow
的帧中的任何地方进行响应
向overlayView
添加UITapGestureRecognizer
并将适当的触摸转发到原始视图控制器(这部分起作用-轻击手势会响应,但是我不知道从哪里去在那里)
使用委托/协议实现告诉原始overlayView
响应触摸
将叠加层及其子视图添加到不是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)
} }
答案 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)
}
}