You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UITableView含按钮单元格点击时索引路径打印不一致问题排查

UITableViewCell复用与按钮点击索引路径错误问题

问题描述

我有一个包含带UIButton的自定义UITableViewCell的UITableView,该UITableView需要每秒重载一次。但快速点击单元格时,有时会打印错误的索引路径。我尝试在prepareForReuse方法中重置索引路径、移除按钮响应关联等清理操作,但无法彻底解决问题。

我发现取消单元格复用或在prepareForReuse中重新创建按钮可以解决问题,但想了解为何仅通过prepareForReuse清理无法达到预期效果。

演示现象:点击第三个单元格时,有时会输出0-4而非0-2;点击第五个单元格时,有时会输出0-6而非0-4。

相关代码

ViewController代码

class ViewController: UIViewController, UITableViewDataSource {
    
    @IBOutlet weak var tableView: UITableView!
    
    var timer: Timer?
    var val = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.dataSource = self
        tableView.register(TableViewCell.self, forCellReuseIdentifier: "cell")
        
        timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateData), userInfo: nil, repeats: true)
        timer?.fire()
    }
    
    deinit {
        timer?.invalidate()
    }
    
    @objc func updateData() {
        val += 1
        tableView.reloadData()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
        cell.indexPath = indexPath
        cell.configure(val: indexPath.row + val)
        cell.delegate = self        
        return cell
    }
}

extension ViewController: TableViewCellDelegate {
    func didTap(cell: UITableViewCell) {
        let indexPath = tableView.indexPath(for: cell)
        print("indexPath = \(indexPath)")
    }
}

Cell代码

protocol TableViewCellDelegate: AnyObject {
    func didTap(cell: UITableViewCell)
}

class TableViewCell: UITableViewCell {
    
    var button: UIButton = UIButton()
    var indexPath: IndexPath?
    weak var delegate: TableViewCellDelegate?
    
    override func prepareForReuse() {
        super.prepareForReuse()
        indexPath = nil
        delegate = nil
        button.removeTarget(nil, action: nil, for: .allTouchEvents)
//        button = UIButton()
    }
    
    func configure(val: Int) {
        if button.superview == nil {
            contentView.addSubview(button)
            button.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                button.topAnchor.constraint(equalTo: contentView.topAnchor),
                button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
                button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
                button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
            ])
        }
        button.setTitle("push me \(val)", for: .normal)
        button.addTarget(self, action: #selector(tap), for: .touchUpInside)
        button.backgroundColor = .red
    }
    
    @objc func tap() {
        delegate?.didTap(cell: self)
    }
}

问题根源分析

问题出在UIButton触摸事件生命周期与UITableView复用的时序冲突:

  1. 快速点击按钮时,iOS会先记录触摸开始事件,但此时单元格还未被复用;在触摸结束(触发touchUpInside)之前,tableView.reloadData()执行,该单元格被复用到新的indexPath位置。
  2. prepareForReuse中的removeTarget和重置delegate操作,是在单元格被复用重新配置之前执行的,但如果触摸事件已经处于“触摸开始”状态,removeTarget无法取消已经触发中的触摸序列——也就是说,按钮的touchUpInside事件依然会绑定到旧的cell实例上执行。
  3. 当tap()方法最终执行时,这个cell已经被复用到新位置,tableView.indexPath(for: cell)会返回当前复用后的新索引,而非点击时的原始索引。

为什么重建按钮/取消复用能解决?

  • 取消复用:每个单元格都是全新实例,不会被复用到其他位置,点击时的cell和当前显示的cell是同一个,索引自然正确。
  • prepareForReuse中重新创建按钮:旧按钮被丢弃,新按钮没有绑定任何未完成的触摸事件,点击只会触发当前配置的事件,不会关联到之前的触摸序列。

正确解决方案(无需取消复用/重建按钮)

不需要重建按钮,只需要确保点击时获取的是单元格被点击时的正确索引,而非依赖复用后的indexPath(for:):

方案1:通过代理直接传递索引

  1. 修改Cell的代理协议和点击方法:
protocol TableViewCellDelegate: AnyObject {
    func didTap(cell: UITableViewCell, indexPath: IndexPath?)
}

class TableViewCell: UITableViewCell {
    // ... 其他代码不变
    @objc func tap() {
        delegate?.didTap(cell: self, indexPath: indexPath)
    }
}
  1. ViewController中直接使用传递的索引:
extension ViewController: TableViewCellDelegate {
    func didTap(cell: UITableViewCell, indexPath: IndexPath?) {
        print("indexPath = \(indexPath)")
    }
}

方案2:优化按钮绑定逻辑(避免重复添加target)

在Cell初始化时只添加一次target,后续仅更新内容,减少冗余绑定:

class TableViewCell: UITableViewCell {
    var button: UIButton = UIButton()
    var indexPath: IndexPath?
    weak var delegate: TableViewCellDelegate?
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupButton()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupButton()
    }
    
    private func setupButton() {
        contentView.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.topAnchor.constraint(equalTo: contentView.topAnchor),
            button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
        button.addTarget(self, action: #selector(tap), for: .touchUpInside)
        button.backgroundColor = .red
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        indexPath = nil
        delegate = nil
    }
    
    func configure(val: Int, indexPath: IndexPath) {
        self.indexPath = indexPath
        button.setTitle("push me \(val)", for: .normal)
    }
    
    @objc func tap() {
        delegate?.didTap(cell: self, indexPath: indexPath)
    }
}

同时修改ViewController的cellForRowAt方法:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
    cell.configure(val: indexPath.row + val, indexPath: indexPath)
    cell.delegate = self        
    return cell
}

内容的提问来源于stack exchange,提问作者wattson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 10:15:59