美文网首页
「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