想象一个大部分是透明的大表格视图,到处都有一些元素。 (也许单元格之间有很大的间隙,或者想象单元格基本上是透明的,只有几个按钮之类的。
在这个几乎透明的桌子后面是一些其他材料,比如一些按钮。
如何制作桌子
如果你滚动它会正常滚动
如果您点击单元格上的按钮,则可以正常工作
但是如果您点击桌子后面的按钮之一,点击会传递并影响该按钮?
(我所说的“点击”是指我们现在所说的“触发的主要操作”——“点击 UIButton”。)
有许多不同的众所周知的技术可以在不同情况下通过视图传递触摸(本身),
等
但我一直无法让上述三个条件发挥作用。
摘要:允许单击 UITableView 后面的 UIButton。
有办法吗?
我发现,通过滚动视图将点击传递到后面的按钮,是一个几乎相同的问题。
以下代码演示了具有透明背景的表格视图的能力,它允许您点击表格视图行中的控件,它允许滚动表格视图,它允许选择表格视图行,并且它允许只要点击位于表格视图行中的任何控件之外,就可以点击表格视图后面的控件。
该演示利用现代单元配置,使用自定义
UIContentConfiguration
和自定义 UIContentView
。它还使用自定义 UITableView
子类。
自定义表格视图子类和自定义单元格内容视图都基于通过 UIViewController 传递触摸提供的解决方案实现自定义命中测试,但进行了一些修改。
首先创建一个新的 iOS 应用程序项目。将项目设置为基于 Swift 和 Storyboard。
以下代码包含大量注释。下面的大部分代码是设置一个工作演示。重要的代码是
hitTest
和PassTableView
中的自定义ButtonContentView
方法。除了这两种方法之外,几乎所有内容都可以根据需要进行更改。
添加一个名为
PassTableView.swift
的新 Swift 文件,其中包含以下内容:
import UIKit
// This subclass of UITableView allows touches to be delegated to another view.
// The table view cells also need to implement the same logic.
// Having the logic in both the cells and the table view allows touches to be delegated if the
// user taps on a cell or if the user taps on an area of the table view not covered by a cell.
class PassTableView: UITableView {
weak var touchDelegate: UIView? = nil
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, with: event) else {
return nil
}
guard view === self, let point = touchDelegate?.convert(point, from: self) else {
return view
}
// If the passthrough view returns a specific view then return that subview
// If the passthrough view returns itself, then return the view that would normally be returned.
// Without that last test, table view scrolling and cell selection is disabled.
if let subview = touchDelegate?.hitTest(point, with: event), subview !== touchDelegate {
return subview
} else {
return view
}
}
}
添加另一个名为
ButtonCell.swift
的 Swift 文件,其中包含以下内容:
import UIKit
fileprivate class ButtonCellView: UIView, UIContentView {
var configuration: UIContentConfiguration {
didSet {
configure(configuration: configuration)
}
}
private var button = UIButton()
init(configuration: UIContentConfiguration) {
self.configuration = configuration
super.init(frame: .zero)
// Give the cell content a semi-transparent background
// This depends on the table view having a clear background
// Optionally, set this to .clear and give the table view a transparent background
backgroundColor = .systemBackground.withAlphaComponent(0.5)
let cfg = UIButton.Configuration.borderedTinted()
button = UIButton(configuration: cfg, primaryAction: UIAction(handler: { action in
print("Button \(self.button.configuration?.title ?? "?") tapped")
}))
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
NSLayoutConstraint.activate([
button.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
button.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
button.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(configuration: UIContentConfiguration) {
guard let configuration = configuration as? ButtonCellConfiguration else { return }
touchDelegate = configuration.touchDelegate
var cfg = button.configuration
cfg?.title = configuration.title
button.configuration = cfg
}
weak var touchDelegate: UIView? = nil
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, with: event) else {
return nil
}
guard view === self, let point = touchDelegate?.convert(point, from: self) else {
return view
}
// If the passthrough view returns a specific view then return that subview
// If the passthrough view returns itself, then return the view that would normally be returned.
// Without that last test, table view scrolling and cell selection is disabled.
if let subview = touchDelegate?.hitTest(point, with: event), subview !== touchDelegate {
return subview
} else {
return view
}
}
}
struct ButtonCellConfiguration: UIContentConfiguration {
var title: String // Used as the button title
weak var touchDelegate: UIView? = nil // The passthrough view to pass touches to
func makeContentView() -> UIView & UIContentView {
return ButtonCellView(configuration: self)
}
func updated(for state: UIConfigurationState) -> ButtonCellConfiguration {
return self
}
}
最后,将提供的
ViewController.swift
的内容替换为以下内容:
import UIKit
class ViewController: UIViewController {
// Use the custom table view subclass so we can support custom hit testing
private lazy var tableView: PassTableView = {
let tv = PassTableView(frame: .zero, style: .plain)
tv.dataSource = self
tv.delegate = self
tv.register(UITableViewCell.self, forCellReuseIdentifier: "buttonCell")
tv.allowsSelection = true
return tv
}()
// This view acts as the touch delegate for the table view and the cell content.
// This view should contain all of the controls you need to handle behind the transparent table view.
// You need to use this extra view since using the table view's superview (self.view)
// as the touch delegate results in infinite recursion in the hitTests.
private lazy var viewLayer: UIView = {
let v = UIView()
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red // Pick a color
// Fill the view controller with the view layer. Adjust as desired.
viewLayer.frame = view.bounds
viewLayer.autoresizingMask = [ .flexibleWidth, .flexibleHeight ]
view.addSubview(viewLayer)
// Add two buttons to the view layer
// The first will be behind rows of the tableview
var cfg = UIButton.Configuration.borderedTinted()
cfg.title = "Background1"
let button1 = UIButton(configuration: cfg, primaryAction: UIAction(handler: { action in
print("Background1 button tapped")
}))
button1.translatesAutoresizingMaskIntoConstraints = false
viewLayer.addSubview(button1)
// The second button will be below the last row (on most devices) but still behind the table view.
// This lets us test touch delegation for buttons behind a row in the table view and for buttons
// behind just the table view.
cfg = UIButton.Configuration.borderedTinted()
cfg.title = "Background2"
let button2 = UIButton(configuration: cfg, primaryAction: UIAction(handler: { action in
print("Background2 button tapped")
}))
button2.translatesAutoresizingMaskIntoConstraints = false
viewLayer.addSubview(button2)
// Position the two background buttons
NSLayoutConstraint.activate([
button1.trailingAnchor.constraint(equalTo: viewLayer.layoutMarginsGuide.trailingAnchor),
button1.centerYAnchor.constraint(equalTo: viewLayer.centerYAnchor),
button2.trailingAnchor.constraint(equalTo: viewLayer.layoutMarginsGuide.trailingAnchor),
button2.bottomAnchor.constraint(equalTo: viewLayer.safeAreaLayoutGuide.bottomAnchor),
])
// Setup the table view's touch delegate
tableView.touchDelegate = self.viewLayer
// Either set the table view background to clear and the cell content to some transparent color, or
// set the table view background to a transparent color and the cell content to clear.
tableView.backgroundColor = .clear
// Fill the view controller with the table view. Adjust as desired.
tableView.frame = view.bounds
tableView.autoresizingMask = [ .flexibleWidth, .flexibleHeight ]
view.addSubview(tableView)
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10 // Partially fill the table view with rows (on most devices). Change as needed.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "buttonCell", for: indexPath)
// Use modern cell configuration
// This is where we set the cell's button title and touch delegate
let cfg = ButtonCellConfiguration(title: "Button \(indexPath.row)", touchDelegate: self.viewLayer)
cell.contentConfiguration = cfg
// Ensure the cell has a clear background
cell.backgroundConfiguration = .clear()
return cell
}
// Demonstrate that cell selection still works as long as the user does not tap on
// any buttons (on the cells or behind the table view).
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected row \(indexPath)")
tableView.deselectRow(at: indexPath, animated: true)
}
}
代码支持iOS 15+。根据需要调整应用程序的部署目标。
构建并运行应用程序。您将看到一个包含 10 行的表视图,每行包含一个按钮。您还会看到另外两个标记为“Background ButtonX”的按钮。两个额外的按钮位于透明表格视图后面。
所有表格视图交互均按预期工作,包括滚动和单元格选择。点击任何按钮,包括表视图后面的两个按钮,都会向控制台打印一条消息。
我已经在代码注释中声明了这一点,但值得重复。至关重要的是,传递给表视图使用的
touchDelegate
的视图和单元格不能位于表视图的超级视图层次结构中,例如self.view
。 touchDelegate
必须是同级(或表兄弟)视图。当点击单元格中的控件外部时,违反此条件将导致无限递归。