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

Swift中UITableView执行performBatchUpdates时单元格消失问题求助

问题原因分析
  • 估算高度与实际高度不匹配:你设置的estimatedRowHeight = 220,但单元格展开后实际高度为200、收起时为40,估算值和真实高度偏差过大,会导致UITableView计算contentSize和单元格位置时出现误差,动态更新高度时极易引发下方单元格布局错乱甚至消失。
  • 选中状态管理混乱:selectedIndex数组仅做添加操作,没有实现“点击切换展开/收起”的逻辑,多次点击同一单元格会导致数组中出现重复索引,虽不直接影响高度判断,但会干扰表格的更新逻辑。
  • 批量更新未精准定位:调用performBatchUpdates(nil)时未指定需要更新的indexPath,UITableView无法精准触发目标单元格的布局更新,进而导致后续单元格位置计算错误。
修复方案

1. 修正估算高度值

将estimatedRowHeight设置为与实际最大高度一致的值,减少布局计算误差:

private lazy var tableView: UITableView = {
    let t = UITableView(frame: .zero, style: .plain)
    t.backgroundColor = .clear
    t.separatorStyle = .none
    t.estimatedRowHeight = 200 // 改为实际最大高度
    t.dataSource = self
    t.delegate = self
    t.register(ProductInfoAttributesCell.self, forCellReuseIdentifier: ProductInfoAttributesCell.reuseIdentifier)
    return t
}()

2. 实现正确的选中状态切换逻辑

改用Set管理选中索引(自动去重),点击时切换单元格的展开/收起状态:

private var selectedIndex: Set<Int> = [] // 替换原数组为Set

// 单元格点击事件处理
cell.onButtonTap = { [weak self] in
    guard let self = self else { return }
    let row = indexPath.row
    if self.selectedIndex.contains(row) {
        self.selectedIndex.remove(row)
    } else {
        self.selectedIndex.insert(row)
    }
    // 精准刷新目标单元格
    self.tableView.performBatchUpdates {
        self.tableView.reloadRows(at: [indexPath], with: .automatic)
    }
}

3. 明确批量更新的刷新范围

在performBatchUpdates闭包中调用reloadRows(at:with:),让UITableView精准更新目标单元格布局,避免全局更新带来的计算偏差。

完整修正代码

ViewController 部分

class ViewController: UIViewController {

    private var items: [String] = Array(repeating: "", count: 50)

    private lazy var tableView: UITableView = {
        let t = UITableView(frame: .zero, style: .plain)
        t.backgroundColor = .clear
        t.separatorStyle = .none
        t.estimatedRowHeight = 200
        t.dataSource = self
        t.delegate = self
        t.register(ProductInfoAttributesCell.self, forCellReuseIdentifier: ProductInfoAttributesCell.reuseIdentifier)
        return t
    }()

    private var selectedIndex: Set<Int> = []

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalTo(view.safeAreaLayoutGuide)
        }
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        selectedIndex.contains(indexPath.row) ? 200 : 40
    }
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(
            withIdentifier: ProductInfoAttributesCell.reuseIdentifier,
            for: indexPath
        ) as! ProductInfoAttributesCell

        cell.onButtonTap = { [weak self] in
            guard let self = self else { return }
            let row = indexPath.row
            if self.selectedIndex.contains(row) {
                self.selectedIndex.remove(row)
            } else {
                self.selectedIndex.insert(row)
            }
            self.tableView.performBatchUpdates {
                self.tableView.reloadRows(at: [indexPath], with: .automatic)
            }
        }

        return cell
    }
}

ProductInfoAttributesCell 部分(无需修改)

// MARK: - ProductInfoAttributesCell
final class ProductInfoAttributesCell: UITableViewCell {

    static let reuseIdentifier = "AccordionTableViewCell"

    var onButtonTap: (() -> Void)?


    let newContainerViewheight = 40
    let newContainerViewheightMax = 90
    let moreButtonheight = 20

    // MARK: - Init

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupLayout()
    }

    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: Setup And Configuration

    private func setupLayout() {
        contentView.clipsToBounds = true
        contentView.backgroundColor = .red
        contentView.layer.borderWidth = 2
        contentView.layer.borderColor = UIColor.black.cgColor
        let tap = UITapGestureRecognizer(target: self, action: #selector(headerContainerViewTapped))
        contentView.addGestureRecognizer(tap)
    }

    @objc private func headerContainerViewTapped() {
        onButtonTap?()
    }

}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.01 13:04:52