Introduction
I expect when I perform "update" on 1 of the CoreData entity content, corresponding UICollectionViewCell
should updated seamlessly.
But, the thing doesn't work as expected.
After debugging, I notice that instead of updating visible UICollectionViewCell
on screen, an invisible offscreen UICollectionViewCell
has been created and all update operations are performed on the invisible UICollectionViewCell
?!
Simple hookup between NSFetchedResultsController and Diffable Data Source
The hookup is pretty straightforward
extension ViewController: NSFetchedResultsControllerDelegate {
func controller(_ fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshotReference: NSDiffableDataSourceSnapshotReference) {
guard let dataSource = self.dataSource else {
return
}
let snapshot = snapshotReference as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
print("dataSource.apply")
dataSource.apply(snapshot, animatingDifferences: true) {
}
}
}
Simple data structure
import Foundation
import CoreData
extension NSTabInfo {
@nonobjc public class func fetchRequest() -> NSFetchRequest<NSTabInfo> {
return NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
}
@NSManaged public var name: String?
@NSManaged public var order: Int64
}
extension NSTabInfo : Identifiable {
}
Simple data source to update cell
private func initDataSource() {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { [weak self] (collectionView, indexPath, objectID) -> UICollectionViewCell? in
guard let self = self else { return nil }
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "cell",
for: indexPath) as? CollectionViewCell else {
return nil
}
guard let nsTabInfo = self.getNSTabInfo(indexPath) else { return nil }
cell.label.text = nsTabInfo.name
print("Memory for cell \(Unmanaged.passUnretained(cell).toOpaque())")
print("Content for cell \(cell.label.text)\n")
return cell
}
)
self.dataSource = dataSource
}
Updating code doesn't work
We perform update on the 1st cell using the following code
@IBAction func updateClicked(_ sender: Any) {
let backgroundContext = self.backgroundContext
backgroundContext.perform {
let fetchRequest = NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "order", ascending: true)
]
do {
let nsTabInfos = try fetchRequest.execute()
if !nsTabInfos.isEmpty {
// Perform update on the first cell
nsTabInfos[0].name = "\(Int(nsTabInfos[0].name!)! + 1)"
if backgroundContext.hasChanges {
try backgroundContext.save()
}
}
} catch {
print("\(error)")
}
}
}
Nothing is changed on the screen. However, if we look at the print output. We can see the initial content ("0") of the 1st cell is updated to "1". But, all of these are being done in an invisible same instance cell.
dataSource.apply
Memory for cell 0x0000000138705cd0
Content for cell Optional("1")
Memory for cell 0x0000000138705cd0
Content for cell Optional("1")
Memory for cell 0x0000000138705cd0
Content for cell Optional("2")
Memory for cell 0x0000000138705cd0
Content for cell Optional("3")
Ordering work without issue
Changing the ordering of the cell works as expected. The following is the code for charging ordering.
@IBAction func moveClicked(_ sender: Any) {
let backgroundContext = self.backgroundContext
backgroundContext.perform {
let fetchRequest = NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "order", ascending: true)
]
do {
let nsTabInfos = try fetchRequest.execute()
for (index, element) in nsTabInfos.reversed().enumerated() {
element.order = Int64(index)
}
if backgroundContext.hasChanges {
try backgroundContext.save()
}
} catch {
print("\(error)")
}
}
}
Do you have idea why such problem occur? I prefer not to have collectionView.reloadData
as workaround, as it will create more issue (like resetting scroll position, cell press state, ...)
I posted a complete demo at https://github.com/yccheok/problem-update-frc-diffable
Thank you