UITableView单元格内UIButton.ConfigurationUpdateHandler响应延迟问题求解
问题描述
在iOS 17.5、iPhone 15 Pro模拟器、Xcode 15.4环境下,同一按钮在两种场景下的交互表现不一致:
- 场景1:按钮放在UITableView单元格的contentView中时,点击需按住一段时间才会切换背景色
- 场景2:按钮直接添加到视图控制器根视图中时,点击可即时切换背景色
场景1(带UITableView)原代码
import UIKit class ViewController: UIViewController { let tableView = UITableView() let checkoutButton = CheckoutButton() override func viewDidLoad() { super.viewDidLoad() // table view setup view.addSubview(tableView) tableView.frame = view.bounds tableView.autoresizingMask = [.flexibleHeight, .flexibleWidth] tableView.dataSource = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.contentView.addSubview(checkoutButton) checkoutButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ checkoutButton.topAnchor.constraint(equalTo: cell.contentView.topAnchor), checkoutButton.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), checkoutButton.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), checkoutButton.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor) ]) return cell } } class CheckoutButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) var configuration = UIButton.Configuration.plain() var attributeContainer = AttributeContainer() attributeContainer.font = .preferredFont(forTextStyle: .headline) attributeContainer.foregroundColor = .label configuration.attributedTitle = .init("Checkout", attributes: attributeContainer) self.configuration = configuration let configHandler: UIButton.ConfigurationUpdateHandler = { button in switch button.state { case .selected, .highlighted: button.configuration?.background.backgroundColor = .systemCyan case .disabled: button.configuration?.background.backgroundColor = .systemGray4 default: button.configuration?.background.backgroundColor = .systemBlue } } self.configurationUpdateHandler = configHandler } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
场景2(无UITableView)原代码
import UIKit class ViewController: UIViewController { let button = CheckoutButton() override func viewDidLoad() { super.viewDidLoad() view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo: view.centerXAnchor), button.centerYAnchor.constraint(equalTo: view.centerYAnchor), button.widthAnchor.constraint(equalToConstant: 300), button.heightAnchor.constraint(equalToConstant: 44) ]) } } class CheckoutButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) var configuration = UIButton.Configuration.plain() var attributeContainer = AttributeContainer() attributeContainer.font = .preferredFont(forTextStyle: .headline) attributeContainer.foregroundColor = .label configuration.attributedTitle = .init("Checkout", attributes: attributeContainer) self.configuration = configuration let configHandler: UIButton.ConfigurationUpdateHandler = { button in switch button.state { case .selected, .highlighted: button.configuration?.background.backgroundColor = .systemCyan case .disabled: button.configuration?.background.backgroundColor = .systemGray4 default: button.configuration?.background.backgroundColor = .systemBlue } } self.configurationUpdateHandler = configHandler } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
问题根源
- 视图复用逻辑错误:场景1中使用全局的
checkoutButton实例,每次单元格复用都会把同一个按钮添加到不同单元格的contentView中,导致视图层级混乱,事件响应链路异常。 - UITableViewCell手势拦截:UITableViewCell默认自带点击选择手势,其触摸事件优先级高于子视图按钮,系统需要额外时间判断触摸目标,导致按钮
highlighted状态触发延迟。
解决方案
1. 修复按钮复用问题
每个单元格需创建独立的CheckoutButton实例,避免重复添加同一视图:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) // 移除单元格中已存在的CheckoutButton,避免复用冲突 cell.contentView.subviews.forEach { subview in if subview is CheckoutButton { subview.removeFromSuperview() } } // 为当前单元格创建新按钮实例 let checkoutButton = CheckoutButton() cell.contentView.addSubview(checkoutButton) checkoutButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ checkoutButton.topAnchor.constraint(equalTo: cell.contentView.topAnchor), checkoutButton.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), checkoutButton.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), checkoutButton.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor) ]) // 禁用单元格选择样式,减少手势冲突 cell.selectionStyle = .none return cell }
2. 提升按钮触摸响应优先级
在CheckoutButton类中重写hitTest方法,确保按钮优先接收触摸事件:
class CheckoutButton: UIButton { // 原有初始化代码... override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // 触摸点在按钮范围内时,直接返回按钮本身,跳过父视图手势判断 if point(inside: point, with: event) { return self } return super.hitTest(point, with: event) } }
3. 手动触发配置更新(可选)
若需更即时的状态切换,可主动监听触摸事件并调用updateConfiguration():
class CheckoutButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) // 原有初始化代码... // 添加触摸事件监听 addTarget(self, action: #selector(onTouchDown), for: .touchDown) addTarget(self, action: #selector(onTouchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel]) } @objc private func onTouchDown() { updateConfiguration() } @objc private func onTouchUp() { updateConfiguration() } // 原有required init代码... }
完整修复后的场景1代码
import UIKit class ViewController: UIViewController { let tableView = UITableView() override func viewDidLoad() { super.viewDidLoad() // table view setup view.addSubview(tableView) tableView.frame = view.bounds tableView.autoresizingMask = [.flexibleHeight, .flexibleWidth] tableView.dataSource = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) // 移除旧按钮 cell.contentView.subviews.forEach { if $0 is CheckoutButton { $0.removeFromSuperview() } } // 创建新按钮 let checkoutButton = CheckoutButton() cell.contentView.addSubview(checkoutButton) checkoutButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ checkoutButton.topAnchor.constraint(equalTo: cell.contentView.topAnchor), checkoutButton.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), checkoutButton.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), checkoutButton.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor) ]) // 禁用单元格选择 cell.selectionStyle = .none return cell } } class CheckoutButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) var configuration = UIButton.Configuration.plain() var attributeContainer = AttributeContainer() attributeContainer.font = .preferredFont(forTextStyle: .headline) attributeContainer.foregroundColor = .label configuration.attributedTitle = .init("Checkout", attributes: attributeContainer) self.configuration = configuration let configHandler: UIButton.ConfigurationUpdateHandler = { button in switch button.state { case .selected, .highlighted: button.configuration?.background.backgroundColor = .systemCyan case .disabled: button.configuration?.background.backgroundColor = .systemGray4 default: button.configuration?.background.backgroundColor = .systemBlue } } self.configurationUpdateHandler = configHandler // 添加触摸事件监听 addTarget(self, action: #selector(onTouchDown), for: .touchDown) addTarget(self, action: #selector(onTouchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel]) } @objc private func onTouchDown() { updateConfiguration() } @objc private func onTouchUp() { updateConfiguration() } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if point(inside: point, with: event) { return self } return super.hitTest(point, with: event) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
内容的提问来源于stack exchange,提问作者Filippo
相关产品推荐
相关产品推荐

