求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)

求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)

你是不是也遇到过这些问题:

  • insertRows 一插就崩:Invalid update: invalid number of rows
  • 多 section 一更新就乱:indexPath 不匹配
  • 想做动画很麻烦:beginUpdates + performBatchUpdates
  • 更新某一条会闪烁:reloadData()
  • 复杂场景(聊天流、瀑布流、Feed 流)代码写到怀疑人生

这些问题的本质是:

你在手动维护 UI 和数据的同步,而 TableView/CollectionView 的 index 一旦不一致,就会瞬间把你崩回桌面。

但自从 iOS 13 开始,Apple 已经给了我们一个"几乎不会崩"的方案:

DiffableDataSource


为什么要用 DiffableDataSource?

一句话:

你只管"数据最终长什么样",UI 自动算出该怎么更新。

它的三大优势:

  1. 不再维护 indexPath
    Diffable 不依赖 index,所有操作基于 item 唯一标识(Hashable),避免大部分 crash。
  2. 动画自动处理
    插入、删除、移动、局部更新都自动生成动画,不再写 batchUpdates
  3. 复杂列表场景刚需
    多 section、聊天流、Feed、搜索、瀑布流......传统方式写起来代码膨胀,Diffable 轻松搞定。

传统 DataSource 容易崩的案例

假设你有一个 users: [User] 的数据源,传统做法:

less 复制代码
users.insert(user, at: index)
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)

问题

  • 异步修改数据时,index 一不对齐就崩
  • 多次 insert/delete 后,indexPath 不匹配
  • batchUpdates 太复杂,容易出错

经典报错:

Invalid update: invalid number of rows in section 0


DiffableDataSource 的安全写法

核心思想:操作 item 标识符,系统自动计算差异并更新 UI

数据模型

csharp 复制代码
struct User: Hashable {
    let id = UUID()
    var name: String
}

Section

arduino 复制代码
enum Section {
    case main
}

DataSource 初始化

swift 复制代码
class ViewController: UIViewController {

    var tableView: UITableView!
    var dataSource: UITableViewDiffableDataSource<section>!
    var users: [User] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView = UITableView(frame: view.bounds, style: .plain)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: &#34;cell&#34;)
        view.addSubview(tableView)

        dataSource = UITableViewDiffableDataSource<section>(tableView: tableView) { tableView, indexPath, user in
            let cell = tableView.dequeueReusableCell(withIdentifier: &#34;cell&#34;, for: indexPath)
            cell.textLabel?.text = user.name
            return cell
        }

        applySnapshot(animated: false)
    }
}

Snapshot 封装

swift 复制代码
func applySnapshot(animated: Bool = true) {
    var snapshot = NSDiffableDataSourceSnapshot<section>()
    snapshot.appendSections([.main])
    snapshot.appendItems(users)
    dataSource.apply(snapshot, animatingDifferences: animated)
}

Diffable 常用操作示例

1. 插入某下标

swift 复制代码
func insertUser(_ user: User, atIndex index: Int) {
    var snapshot = dataSource.snapshot()
    var items = snapshot.itemIdentifiers(inSection: .main)
    let safeIndex = max(0, min(index, items.count))

    if safeIndex == items.count {
        snapshot.appendItems([user], toSection: .main)
    } else {
        let before = items[safeIndex]
        snapshot.insertItems([user], beforeItem: before)
    }

    users.insert(user, at: safeIndex)
    dataSource.apply(snapshot, animatingDifferences: true)
}

2. 删除某下标

swift 复制代码
func deleteUser(atIndex index: Int) {
    var snapshot = dataSource.snapshot()
    let items = snapshot.itemIdentifiers(inSection: .main)
    guard items.indices.contains(index) else { return }

    let itemToDelete = items[index]
    snapshot.deleteItems([itemToDelete])
    users.remove(at: index)

    dataSource.apply(snapshot, animatingDifferences: true)
}

3. 移动 item

css 复制代码
func moveItem(from fromIndex: Int, to toIndex: Int) {
    var snapshot = dataSource.snapshot()
    var items = snapshot.itemIdentifiers(inSection: .main)
    guard items.indices.contains(fromIndex),
          items.indices.contains(toIndex) else { return }

    let item = items.remove(at: fromIndex)
    items.insert(item, at: toIndex)

    snapshot.deleteSections([.main])
    snapshot.appendSections([.main])
    snapshot.appendItems(items, toSection: .main)

    let moved = users.remove(at: fromIndex)
    users.insert(moved, at: toIndex)

    dataSource.apply(snapshot, animatingDifferences: true)
}

4. 更新 item 的字段(安全写法)

scss 复制代码
func updateUserName(atIndex index: Int, newName: String) {
    var snapshot = dataSource.snapshot()
    let items = snapshot.itemIdentifiers(inSection: .main)
    guard items.indices.contains(index) else { return }

    let oldItem = items[index]
    let updatedItem = User(id: oldItem.id, name: newName)

    // 更新本地 users 数组
    users[index] = updatedItem

    // 判断是否有下一个 item 可作为插入参照
    if index + 1 < items.count {
        let nextItem = items[index + 1]
        snapshot.deleteItems([oldItem])
        snapshot.insertItems([updatedItem], beforeItem: nextItem)
    } else {
        // 如果是最后一个,直接删除再 append
        snapshot.deleteItems([oldItem])
        snapshot.appendItems([updatedItem], toSection: .main)
    }

    dataSource.apply(snapshot, animatingDifferences: true)
}

✅ 不依赖自定义 safe 下标

✅ 自动处理最后一个 item

✅ 保持 id 不变,动画安全


总结

DiffableDataSource 的核心优势:

  • 不再维护 indexPath → 避免崩溃
  • 动画自动生成 → 插入/删除/移动/更新一气呵成
  • 复杂场景稳如老狗 → 多 section、Feed、聊天、搜索、瀑布流都轻松

一句话:Diffable 是 2025 年 iOS 列表开发的标配。

不用它,你会花大量时间调 index;

用了它,你会怀疑自己以前为什么受苦。


相关推荐
徐小夕2 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx2 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
fxshy2 小时前
Cursor 前端Global Cursor Rules
前端·cursor
红彤彤2 小时前
前端接入sse(EventSource)(@fortaine/fetch-event-source)
前端
WindStormrage3 小时前
umi3 → umi4 升级:踩坑与解决方案
前端·react.js·cursor
十一.3663 小时前
103-105 添加删除记录
前端·javascript·html
用户47949283569153 小时前
面试官:DNS 解析过程你能说清吗?DNS 解析全流程深度剖析
前端·后端·面试
涔溪3 小时前
微前端中History模式的路由拦截和传统前端路由拦截有什么区别?
前端·vue.js