美文网首页
「Mac」WWDC源码阅读 ——高效的 TableView 数据

「Mac」WWDC源码阅读 ——高效的 TableView 数据

作者: fuyoufang | 来源:发表于2020-04-24 09:06 被阅读0次

TableView 中的数据经常会刷新,比如:在用户下拉刷新时、更改了搜索条件时。这时我们就需要从服务器上重新获取数据,然后再刷新界面。

我之前的做法比较简单粗暴:在获取到新的数据之后,直接调用 TableView 的 reload 方法就可以了。

在阅读 WWDC 源码时,看见刷新 TableView 数据的代码,感觉很受启发。作者为了避免 TableView 刷新不必要的 cell,先对数据进行了分心,分别找出了需要删除的、新增的,更新的数据,然后针对性对 TableView 中的 cell 进行处理。而不是一股脑的 reload。

下面是具体代码(删除了一些不相干的代码):

  1. 创建 IndexedSessionRow,用于将数据和数据在 TableView 中对应的 cell 的 index 进行对应存储。这样就可以知道每个数据对应的位置
struct IndexedSessionRow: Hashable {
    let sessionRow: SessionRow
    let index: Int
    func hash(into hasher: inout Hasher) {
        hasher.combine(sessionRow)
    }
    static func == (lhs: IndexedSessionRow, rhs: IndexedSessionRow) -> Bool {
        return lhs.sessionRow == rhs.sessionRow
    }
}
  1. 更新数据的方法

更新数据时,将 TableView 原来的 cell 进行分类:

  • 移除的 旧数据中存在,新数据中不存在
  • 增加的 旧数据中不存在,新数据中存在
  • 更新的 旧数据中存在,并且在新数据中也存在,但是数据的相对位置不一致

下面是具体代码:

func setDisplayedRows(_ newValue: [SessionRow], animated: Bool) {
    
    // Dismiss the menu when the displayed rows are about to change otherwise it will crash
    tableView.menu?.cancelTrackingWithoutAnimation()
    displayedRowsLock.async {
        let oldValue = self.displayedRows
        // Same elements, same order: https://github.com/apple/swift/blob/master/stdlib/public/core/Arrays.swift.gyb#L2203
        // 如果数据相同,顺序相同,则直接返回
        if oldValue == newValue { return }
        // 生成旧数据列表的 Set
        let oldRowsSet = Set(oldValue.enumerated().map { IndexedSessionRow(sessionRow: $1, index: $0) })
        // 新数据列表的 Set
        let newRowsSet = Set(newValue.enumerated().map { IndexedSessionRow(sessionRow: $1, index: $0) })
        // 获取到需要删除的 Set
        let removed = oldRowsSet.subtracting(newRowsSet)
        // 获取到需要添加的 Set
        let added = newRowsSet.subtracting(oldRowsSet)
        let removedIndexes = IndexSet(removed.map { $0.index })
        let addedIndexes = IndexSet(added.map { $0.index })
        // Only reload rows if their relative positioning changes. This prevents
        // cell contents from flashing when cells are unnecessarily reloaded
        // 只有到相对位置改变的时候才需要刷新。
        // 这可以避免 cell 在内容不改变的时候进行不必要的刷新
        var needReloadedIndexes = IndexSet()
        // old 中需要保留的
        let sortedOldRows = oldRowsSet.intersection(newRowsSet).sorted(by: { (row1, row2) -> Bool in
            return row1.index < row2.index
        })
        // new 中非新增的
        let sortedNewRows = newRowsSet.intersection(oldRowsSet).sorted(by: { (row1, row2) -> Bool in
            return row1.index < row2.index
        })
        // sortedOldRows 和 sortedNewRows 中的数据相同,但是因为数据可能不相同
        for (oldSessionRowIndex, newSessionRowIndex) in zip(sortedOldRows, sortedNewRows) where oldSessionRowIndex.sessionRow != newSessionRowIndex.sessionRow {
            needReloadedIndexes.insert(newSessionRowIndex.index)
        }
        DispatchQueue.main.sync {
            // Preserve selected rows
            let selectedRows = self.tableView.selectedRowIndexes.compactMap { (index) -> IndexedSessionRow? in
                guard index < oldValue.endIndex else { return nil }
                return IndexedSessionRow(sessionRow: oldValue[index], index: index)
            }
            var selectedIndexes = IndexSet(newRowsSet.intersection(selectedRows).map { $0.index })
            if selectedIndexes.isEmpty, let defaultIndex = newValue.firstSessionRowIndex() {
                selectedIndexes.insert(defaultIndex)
            }
            NSAnimationContext.beginGrouping()
            let context = NSAnimationContext.current
            if !animated {
                context.duration = 0
            }
            context.completionHandler = {
                NSAnimationContext.runAnimationGroup({ (context) in
                    context.allowsImplicitAnimation = true
                    self.tableView.scrollRowToCenter(selectedIndexes.first ?? 0)
                }, completionHandler: nil)
            }
            self.tableView.beginUpdates()
            self.tableView.removeRows(at: removedIndexes, withAnimation: [NSTableView.AnimationOptions.slideLeft])
            self.tableView.insertRows(at: addedIndexes, withAnimation: [NSTableView.AnimationOptions.slideDown])
            // insertRows(::) and removeRows(::) will query the delegate for the row count at the beginning
            // so we delay updating the data model until after those methods have done their thing
            self.displayedRows = newValue
            // This must be after you update the backing model
            self.tableView.reloadData(forRowIndexes: needReloadedIndexes, columnIndexes: IndexSet(integersIn: 0..<1))
            self.tableView.selectRowIndexes(selectedIndexes, byExtendingSelection: false)
            self.tableView.endUpdates()
            NSAnimationContext.endGrouping()
        }
    }
}

其中用到了数据集合的方法,subtracting。下面是数据结合的方法:

  • intersection(_ :) 创建一个只包含两个公共值的新集合。(交集)
  • symmetricDifference(_ :) 创建一个新集合,其值集在两个集合中,但不能同时存在。(非交集)
  • union(_ :) 创建一个包含两个集合中的所有值的新集合。(合集)
  • subtracting(_ :) 创建一个值不在指定集中的新集。(补集)

还用到了 zip 方法,用于将两个数组合并一个数组,具体用法可以看 Swift - zip函数使用详解

相关文章

网友评论

      本文标题:「Mac」WWDC源码阅读 ——高效的 TableView 数据

      本文链接:https://www.haomeiwen.com/subject/ghkxwhtx.html