Swift UITableView可复用单元格滚动显示错乱,重载即可恢复
问题根源定位
出现「首次加载列表显示正常、滚动后内容错乱、重新触发列表重载后又恢复正常」的核心原因,完全是UITableView复用机制带来的逻辑不匹配,具体有两个问题:
- 依赖外部临时变量
tempDate判断日期头的逻辑完全不适配复用场景:cellForRowAt的调用顺序不是按行号从0到末尾依次执行的。首次加载列表时,tableView按从上到下的顺序依次渲染cell,tempDate刚好能存下上一行的日期,判断结果碰巧正确;但滚动列表时,tableView会直接定位到可视区域对应的位置,复用现成的cell,不会依次执行中间行的方法,此时*tempDate* 存的是完全脱离当前列表上下文的旧值,判断结果必然出错。 - 复用取出的cell会携带上一个使用场景的控件状态:从复用队列拿到的cell保留了之前给其他indexPath设置的
textLabel、detailTextLabel的内容,没有基于当前行的数据做全量重置,自然会出现显示错乱。 - 观察到的「重载后显示恢复正常」,本质是重载时tableView又回到了从上到下顺序渲染cell的流程,
tempDate的临时判断逻辑碰巧又能跑对,并不是问题真的被修复了。
解决方案
调整判断逻辑,抛弃跨方法存储的临时变量
判断是否显示日期头的逻辑,必须和当前indexPath绑定,基于相邻行的真实数据做对比,不能依赖方法外部存储的临时值。正确的判断规则为:
- 如果当前行是所在section的第0行,必须显示日期头
- 如果是section内的后续行,取同section上一行的日期,和当前行日期做对比,不一致才显示日期头,否则隐藏
- 小优化:如果你的section本身就是按日期分组的(即同一个section下所有行的日期完全一致),那判断逻辑可以简化为仅
indexPath.row == 0时显示日期头,其余情况直接清空textLabel,不需要逐行对比日期,性能更好。
拿到复用cell后,全量显式设置所有展示控件的状态
不管是显示还是隐藏的状态,都要基于当前行的判断结果直接赋值,不能遗留之前cell的旧状态。
修正后的参考代码:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "historyGoalCell", for: indexPath) // 取出当前行对应的模型,减少重复数组下标写法 let currentModel = items[indexPath.section][indexPath.row] let currentDateStr = dateManager.dateAsString(for: currentModel.date!) // 判断是否需要显示日期头 var showDateHeader = false if indexPath.row == 0 { showDateHeader = true } else { // 取同section上一行的模型做日期对比 let lastModel = items[indexPath.section][indexPath.row - 1] let lastDateStr = dateManager.dateAsString(for: lastModel.date!) showDateHeader = (currentDateStr != lastDateStr) } // 全量设置cell的展示内容,无论最终状态是显示还是隐藏都显式赋值 cell.textLabel?.text = showDateHeader ? currentDateStr : "" cell.detailTextLabel?.text = "\(currentDateStr), \(currentModel.name ?? "")" return cell }
复用机制避坑提醒
- 永远不要在
cellForRowAt方法里依赖外部成员变量存跨行判断的临时值,这个方法的调用时机完全由tableView的可视区域决定,不受代码执行顺序控制。 - 如果后续使用自定义UITableViewCell,一定要在自定义类里重写
prepareForReuse方法,把所有控件的文字、状态重置为默认值,从源头避免复用带来的旧值残留问题。
内容的提问来源于stack exchange,提问作者jat
相关产品推荐
相关产品推荐

