如何通过 UITableView 允许对后面的按钮进行单击?

问题描述 投票:0回答:1

想象一个大部分是透明的大表格视图,到处都有一些元素。 (也许单元格之间有很大的间隙,或者想象单元格基本上是透明的,只有几个按钮之类的。

在这个几乎透明的桌子后面是一些其他材料,比如一些按钮。

如何制作桌子

  • 如果你滚动它会正常滚动

  • 如果您点击单元格上的按钮,则可以正常工作

  • 但是如果您点击桌子后面的按钮之一,点击会传递并影响该按钮?

(我所说的“点击”是指我们现在所说的“触发的主要操作”——“点击 UIButton”。)

有许多不同的众所周知的技术可以在不同情况下通过视图传递触摸(本身),

但我一直无法让上述三个条件发挥作用。

摘要:允许单击 UITableView 后面的 UIButton。

有办法吗?


我发现,通过滚动视图将点击传递到后面的按钮,是一个几乎相同的问题。

ios swift uitableview uikit event-bubbling
1个回答
0
投票

以下代码演示了具有透明背景的表格视图的能力,它允许您点击表格视图行中的控件,它允许滚动表格视图,它允许选择表格视图行,并且它允许只要点击位于表格视图行中的任何控件之外,就可以点击表格视图后面的控件。

该演示利用现代单元配置,使用自定义

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
必须是同级(或表兄弟)视图。当点击单元格中的控件外部时,违反此条件将导致无限递归。

© www.soinside.com 2019 - 2024. All rights reserved.