I'm trying to use NSFetchController with a CollectionView. My problem is when I change a property value for my NSManagedObject, my cells are not updating correctly. Here is a link to better show what I am talking about. Okay, so if you watch the video you can see basically what the app does. It has a bunch of hero's and when you click a cell it goes to another view controller and shows a picture, name and publisher. Now you were also able to see the problem, when I changed the name too 'Amanda Doe' and saved it. It was inserted in the right place at the beginning, but the cell label wasn't updated.
When I click the cell again it shows Amanda Doe on the navigation title, but the cell label is still the same. In order for the name to show I have to restart the app or click the same cell and just click save again.
This is how my NSFetchResultController looks like. I sort by the Publisher name then by the Hero name. It also has multiple sections based on the publisher.
private var blockOperations: [BlockOperation] = []
lazy var heroController: NSFetchedResultsController<Hero> = {
let request: NSFetchRequest<Hero> = Hero.fetchRequest()
let nameSort = NSSortDescriptor(key: "name", ascending: true)
let publisherSort = NSSortDescriptor(key: "publisher.name", ascending: true)
request.sortDescriptors = [publisherSort, nameSort]
let context = CoreDataManager.shared.persistentContainer.viewContext
let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "publisher.name", cacheName: nil)
controller.delegate = self
do {
try controller.performFetch()
} catch let err {
print("Unable to fetch heros from controller.", err)
}
return controller
}()
This code here is basically the boilerplate code that I found that most people use to get the NSFetchResultController to work with the CollectionView.
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
blockOperations.removeAll(keepingCapacity: false)
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let op: BlockOperation
switch type {
case .delete:
op = BlockOperation { self.collectionView.deleteSections(IndexSet(integer: sectionIndex))}
blockOperations.append(op)
case .insert:
op = BlockOperation { self.collectionView.insertSections(IndexSet(integer: sectionIndex))}
blockOperations.append(op)
case .move:
break
case .update:
op = BlockOperation { self.collectionView.reloadSections(IndexSet(integer: sectionIndex))}
blockOperations.append(op)
@unknown default:
fatalError()
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType,newIndexPath: IndexPath?) {
let op: BlockOperation
switch type {
case .insert:
guard let newIndexPath = newIndexPath else { return }
op = BlockOperation { self.collectionView.insertItems(at: [newIndexPath]) }
case .delete:
guard let indexPath = indexPath else { return }
op = BlockOperation { self.collectionView.deleteItems(at: [indexPath]) }
case .move:
guard let indexPath = indexPath, let newIndexPath = newIndexPath else { return }
op = BlockOperation { self.collectionView.moveItem(at: indexPath, to: newIndexPath) }
case .update:
guard let indexPath = indexPath else { return }
op = BlockOperation { self.collectionView.reloadItems(at: [indexPath]) }
@unknown default:
fatalError()
}
blockOperations.append(op)
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView.performBatchUpdates({
self.blockOperations.forEach { $0.start() }
}, completion: { finished in
self.blockOperations.removeAll(keepingCapacity: false)
})
}
So am I missing something, is it because I am missing .move case on the section? I was playing around with the app and found out if the cell doesn't move or have to move to a new index path it works fine. Why is that? Would really appreciate any help. Thanks.
EDIT 1
After playing around with the code some more, I came across another problem. Problem 1 is as you seen in the video. Cell is not getting updated correctly and I don't know why. I tried to debug it as @pbasdf suggested and found out this. First when I change the name to 'Amanda Doe' it triggers the didChangeAnObject: .move case twice but never the didChangeAnObject: .update. When I click on the same cell again and change the name to 'Amanda Doee' it calls didChangeAnObject: .update and updates the cell with the correct name. So basically if the didChangeAnObject: .move case is called the cell doesn't update and if the didChangeAnObject: .update case is called it updates the cell with the correct name.
Problem 2 is when I change the publisher name. So for now the name is 'Amanda Doee' and publisher name 'ABC Studios'. Now if I just change the publisher name to 'ABC Studioss' it should move the cell to a new section called 'ABC Studioss' because of my NSFetchResultController. The thing is it doesn't do that it stays in the same position and it calls the didChangeAnObject: .update case.
Now I found a way that it works. In order for it to work I need to change the Name & Publisher. It won't work if I just change the publisher. So I would have to change 'Amanda Doee' to 'Amanda Doeee' and 'ABC Studios' to 'ABC Studioss'. This is how the methods get called when I change the name and publisher and save it. didChangeSectionInfo: .insert, didChangeAnObject: .move, didChangeSectionInfo: .insert, didChangeAnObject: .move. Now if I was to just change the name of the publisher this only gets called didChangeAnObject: .update. I would have to relaunch my app to see the changes with a new section called 'ABC Studioss' with 'Amanda Doee'.
So I really don't know what is going on & why it is behaving like this.
Edit 2
Here is the code I use in the other VC to save edits or create a new hero.
@objc private func handleSave() {
let context = CoreDataManager.shared.persistentContainer.viewContext
if let hero = hero {
hero.name = nameTF.text ?? ""
hero.publisher?.name = publisherTF.text
do {
try context.save()
}
catch let err {
print("Unable to save hero edits.", err)
}
}
else {
print("Create new hero.")
let hero = Hero(context: context)
hero.name = nameTF.text
let publisher = Publisher(context: context)
publisher.name = publisherTF.text
hero.publisher = publisher
guard let imageData = heroImageView.image?.jpegData(compressionQuality: 1) else {
try? context.save()
return
}
let photo = Photo(context: context)
photo.data = imageData
hero.photo = photo
try? context.save()
}
navigationController?.popViewController(animated: true)
}