TableView 中的数据经常会刷新,比如:在用户下拉刷新时、更改了搜索条件时。这时我们就需要从服务器上重新获取数据,然后再刷新界面。
我之前的做法比较简单粗暴:在获取到新的数据之后,直接调用 TableView 的 reload 方法就可以了。
在阅读 WWDC 源码时,看见刷新 TableView 数据的代码,感觉很受启发。作者为了避免 TableView 刷新不必要的 cell,先对数据进行了分心,分别找出了需要删除的、新增的,更新的数据,然后针对性对 TableView 中的 cell 进行处理。而不是一股脑的 reload。
下面是具体代码(删除了一些不相干的代码):
- 创建 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
}
}
- 更新数据的方法
更新数据时,将 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函数使用详解
网友评论