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

求助:UITableViewDiffableDataSource应用快照后reconfiguredItemIdentifiers未清空

UITableViewDiffableDataSource调用reconfigureItems后全部条目被更新的问题

问题现象

在UIKit项目中使用UITableViewDiffableDataSource时,调用.reconfigureItems(_:)或.reloadItems(_:)想要仅更新特定单元格,但操作后整个UITableView的所有条目都会被重新配置。排查发现调用.apply(_:)应用快照时,snapshot.reconfiguredItemIdentifiers属性仍保留着之前操作的条目ID。

复现代码

ViewController代码

class ViewController: UIViewController {

    @IBOutlet var tableView: UITableView!
    
    var activitiesDataSource: [Activity]!
    var snapshot = NSDiffableDataSourceSnapshot<Int, Activity.ID>()
    
    lazy var diffableDataSource = UITableViewDiffableDataSource<Int, Activity.ID>(tableView: tableView) { tableView, indexPath, itemIdentifier in
        let activity = self.getActivity(by: itemIdentifier)
        print("setting up cell with title: \(activity.title)")
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
        cell.titleLabel.text = activity.title
        return cell
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        activitiesDataSource = [Activity(title: "first"), Activity(title: "second"), Activity(title: "third")]
        tableView.delegate = self
        tableView.dataSource = diffableDataSource
        tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "CustomCell")
        
        prepareSnapshot()
    }
    
    func prepareSnapshot() {
        let activitiesIds = activitiesDataSource.map(\.id)
        snapshot.appendSections([0])
        snapshot.appendItems(activitiesIds)
        diffableDataSource.apply(snapshot)
    }
        
    func getActivity(by id: UUID) -> Activity {
        guard let activity = self.activitiesDataSource.first(where: { $0.id == id }) else {
            fatalError("Could not find activity by id")
        }
        return activity
    }
}

extension ViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let activityId = snapshot.itemIdentifiers[indexPath.row]
        let activity = getActivity(by: activityId)
        activity.title += " tap"
        snapshot.reconfigureItems([activityId])
        diffableDataSource.apply(snapshot)
    }
}

Activity类代码

class Activity: Identifiable {
    
    let id = UUID()
    var title: String
    
    init(title: String) {
        self.title = title
    }
}

实际输出与预期

依次点击第一、第二、第三单元格后,实际输出:

setting up cell with title: first tap
setting up cell with title: first tap
setting up cell with title: second tap
setting up cell with title: first tap
setting up cell with title: second tap
setting up cell with title: third tap

预期输出:

setting up cell with title: first tap
setting up cell with title: second tap
setting up cell with title: third tap

问题原因

你复用了全局的同一个snapshot实例,没有每次创建新快照或重置快照的配置标记:

  • 每次调用snapshot.reconfigureItems(_:)后,该快照的reconfiguredItemIdentifiers会持久保存这些需要重新配置的条目ID。
  • 当你再次调用diffableDataSource.apply(snapshot)时,之前的标记并未被清除,导致DiffableDataSource认为之前标记的条目也需要重新配置,最终所有历史标记过的条目都会被重复设置。

解决方法

推荐方案:每次操作创建新的快照实例

DiffableDataSource的设计核心是基于快照状态的对比,正确的做法是每次更新时创建新的快照,或者基于当前数据源的快照进行修改:

  1. 修改prepareSnapshot方法,每次创建新快照:
func prepareSnapshot() {
    var snapshot = NSDiffableDataSourceSnapshot<Int, Activity.ID>()
    let activitiesIds = activitiesDataSource.map(\.id)
    snapshot.appendSections([0])
    snapshot.appendItems(activitiesIds)
    diffableDataSource.apply(snapshot)
}
  1. 修改didSelectRowAt方法,基于当前数据源的快照进行修改:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // 从数据源获取当前的快照,而不是用全局快照
    let currentSnapshot = diffableDataSource.snapshot()
    let activityId = currentSnapshot.itemIdentifiers[indexPath.row]
    let activity = getActivity(by: activityId)
    activity.title += " tap"
    
    // 创建新快照并标记需要更新的条目
    var updatedSnapshot = currentSnapshot
    updatedSnapshot.reconfigureItems([activityId])
    diffableDataSource.apply(updatedSnapshot)
}

备选方案:复用全局快照时重置配置标记(不推荐)

如果坚持复用全局快照,每次调用reconfigureItems前需要清除之前的标记,但这种方式容易遗漏其他状态,不推荐:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let activityId = snapshot.itemIdentifiers[indexPath.row]
    let activity = getActivity(by: activityId)
    activity.title += " tap"
    
    // 先清除之前的所有reconfigure标记
    snapshot.reconfigureItems([])
    // 再标记当前需要更新的条目
    snapshot.reconfigureItems([activityId])
    diffableDataSource.apply(snapshot)
}

验证结果

修改后再次依次点击三个单元格,输出将与预期一致,每个单元格仅会被配置一次。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 16:30:15