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
相关产品推荐
相关产品推荐

