在NSFetchedResultsControllerDelegate中,有一個代理方法:
optional func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshot)
這個代理方法是為UITableViewDiffableDataSourceh和UICollectionViewDiffableDataSource量身定制的,目的是便于更加簡單的處理數(shù)據(jù)的更新。
首先:我們創(chuàng)建一個FRC:
private func setupFetchedResultsController() {
fetchedResultsController = DatabaseManager.createTodoItemsFetchedResultsController()
fetchedResultsController?.delegate = self
try? fetchedResultsController?.performFetch()
}
創(chuàng)建UITableViewDiffableDataSource:
private func setupDataSource() {
dataSource = TodoListDiffableDataSource<String, NSManagedObjectID>(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, objectID in
guard let cell = tableView.dequeueReusableCell(withIdentifier: TodoListCell.reuseIdentifier, for: indexPath) as? TodoListCell else {
fatalError("Cannot dequeue TodoListCell")
}
let context = self?.fetchedResultsController?.managedObjectContext
let todoItem = context?.object(with: objectID) as? TodoItem
cell.configure(item: todoItem, shouldShowCheckmark: true)
return cell
})
}
實現(xiàn)NSFetchedResultsControllerDelegate
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
guard let dataSource = dataSource else { return }
dataSource.apply(snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>, animatingDifferences: false)
}
新增一個實體:
let item = DatabaseManager.shared.createTodoItem()
item.title = "試試"
item.isCompleted = false
try? DatabaseManager.shared.saveContext()
當調(diào)用saveContext()時,會觸發(fā)FRC的controller(:didChangeContentWith:),如果此時cell是可見的,能正常展示,但是如果后續(xù)再次刷新tableView或者滑動tableView使得cell刷新,那么數(shù)據(jù)將會為空,這一切都是由于controller(:didChangeContentWith:)方法中,snapshot中的objectID是臨時ID導致,下面我直接給出這個方法的詳解:
/**
1. 這個代理方法一旦實現(xiàn),其余傳統(tǒng)的代理方法將全部失效。
2. `snapshot`,蘋果定義為 `NSDiffableDataSourceSnapshot<String, NSManagedObjectID>` 類型。
3. 當新建一個 NSManagedObject 子類實體(以下簡稱 MO)時,Core Data 會為該實體分配一個臨時 objectID,
只有在真正提交到數(shù)據(jù)庫之后,系統(tǒng)才會將臨時 objectID 轉(zhuǎn)化為永久的 objectID,同時可能將臨時 objectID 綁定的 MO 銷毀。
本方法的 `snapshot` 中新增的實體的 objectID 就是臨時的,換句話說,這個代理方法是在saveContext()時,臨時ID轉(zhuǎn)化為永久ID之前觸發(fā)。
所以這會導致這樣一種現(xiàn)象:
當這個代理方法觸發(fā)時,dataSource 調(diào)用 `apply()` 進而觸發(fā) tableView 刷新,
因為此時臨時ID還沒轉(zhuǎn)化為永久ID,臨時 objectID 綁定的 MO 還未銷毀,所以此時在 `cellProvider` 中通過臨時 objectID 還是可以正常取到 MO 的,tableView 能正常展示,
而當某一刻再次刷新 tableView 或者手動滑動 tableView 使得 cell 刷新,會發(fā)現(xiàn)新增的 MO 數(shù)據(jù)全部為空,這是因為刷新 tableView 之前,臨時ID已經(jīng)成功轉(zhuǎn)化為了永久ID
也就是臨時 objectID 綁定的 MO 已經(jīng)被銷毀了,再通過臨時 objectID 就取不到了。
要解決這個問題:
方法一(推薦):可以在 `save` 之前,先獲取永久 ID,如:`context.obtainPermanentIDs(for: [MO])`.
方法二: 通過 child context 創(chuàng)建 MO,這樣 `save` 的時候,就只會提交到 parent context,
經(jīng)測試,即便最后由root context提交到數(shù)據(jù)庫,只要是child context創(chuàng)建的MO,MO都不會被銷毀.
方法三: 在每次 `save` 之后,調(diào)用一遍 `performFetch()`,`performFetch()` 會從數(shù)據(jù)庫中抓取數(shù)據(jù)存在 context 中,
會自動觸發(fā) `controller(_:didChangeContentWith:)`,這個時候的 objectID 肯定是永久的。但這種方式比較蠻力,不推薦。
警告:不要在 `controller(_:didChangeContentWith:)` 中調(diào)用 `performFetch()`,會發(fā)生死循環(huán)。
4. 有了這個代理方法,我們無需自己初始化快照,直接用本方法提供的 `snapshot` 即可。
5. 如果想自己創(chuàng)建快照,比如 `NSDiffableDataSourceSnapshot<S, T>`,通過本方法更新快照,
需要將 `NSDiffableDataSourceSnapshot<String, NSManagedObjectID>` 轉(zhuǎn)化為 `NSDiffableDataSourceSnapshot<S, T>`
*/
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference)