Apparently, NSFetchedResultsController
doesn't know data was changed on a given NSManagedObjectContext
when that change happens by executing a NSBatchUpdateRequest
. Is it possible to force NSFetchedResultsController
to reload its data so controller(_:didChange:at:for:newIndexPath:)
on NSFetchedResultsControllerDelegate
gets called?
I'm using a fetched results controller to be informed about changes in the underlying data so I can update my table view when it changes. I'm adopting the NSFetchedResultsControllerDelegate
protocol and implementing the controller(_:didChange:at:for:newIndexPath:)
method. Everything seems to be working, because the did change method it's called when I insert, delete or update individual items.
The problem happens when I'm batch-updating several items with NSBatchUpdateRequest
. When I do that, the delegate method controller(_:didChange:at:for:newIndexPath:)
is not called (I'm executing these updates in the same context used by the fetched results controller).
#1: Fetched results controller initialization:
func createFetchedResultsController() -> NSFetchedResultsController {
let fetchRequest: NSFetchRequest<EAlbum> = NSFetchRequest<EAlbum>(entityName: "EAlbum")
fetchRequest.predicate = NSCompoundPredicate(type: .and, subpredicates: createPredicates())
addSortDescriptors(on: fetchRequest)
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: AppDelegate.myPersistentContainer.viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}
#2: NSFetchedResultsControllerDelegate:
extension FeedTableViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?) {
switch type {
case .insert:
if let indexPath = newIndexPath {
tableView.insertRows(at: [indexPath], with: .fade)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .fade)
}
default:
// TODO
}
}
}
#3: Function where I use NSBatchUpdateRequest
to update a bunch of items from AppDelegate.myPersistentContainer.viewContext
:
func markAllAsRead(using context: NSManagedObjectContext) -> Bool {
let request = NSBatchUpdateRequest(entityName: "EAlbum")
request.predicate = NSPredicate(format: "%K == YES", #keyPath(EAlbum.markUnread))
request.propertiesToUpdate = [#keyPath(EAlbum.markUnread): false]
request.resultType = .statusOnlyResultType
do {
let result = try context.execute(request) as? NSBatchUpdateResult
return result?.result as? Bool ?? false
} catch {
return false
}
}
#4: Function where I update just one item from AppDelegate.myPersistentContainer.viewContext
:
func updateUnreadFlag(albumId: String, markUnread: Bool, using context: NSManagedObjectContext) {
guard let album = try? EAlbum.findAlbum(matching: albumId, in: context) else { return }
album.markUnread = markUnread
do {
try context.save()
} catch {
// Handle error
}
}
When #4 is called, the delegate controller(_:didChange:at:for:newIndexPath:)
method is called. But when executing #3, the delegate method does not get called. It seems like NSFetchedResultsController
doesn't know that data was changed on a given NSManagedObjectContext
when that change happens by executing a NSBatchUpdateRequest
.
Since my fetched results controller doesn't know that data was changed, I thought about reloading it by hand like so:
func reloadFetchedResultsController() {
fetchedResultsController = createFetchedResultsController()
do {
try fetchedResultsController.performFetch()
} catch {
// Handle error
}
tableView.reloadData()
}
So everytime after #3 is executed, I would call reloadFetchedResultsController()
to reload the fetched results controller and the table view. But for some reason it seems that it loads the old data, because nothing changes on my table view.
A: Is it possible to let a fetched results controller know that data was changed when executing a batch update?
B: Is it possible to force my fetched results controller to reload its data? Because, as a last resort, I could force a reload on my fetched results controller after executing #3, and finally force a table view update after that, so it would reload it's cells using the newly batch-updated data from the fetched results controller.
From the documentation:
After executing the batch update request you have to merge the changes into the context.
Try
func markAllAsRead(using context: NSManagedObjectContext) -> Bool {
let request = NSBatchUpdateRequest(entityName: "EAlbum")
request.predicate = NSPredicate(format: "%K == YES", #keyPath(EAlbum.markUnread))
request.propertiesToUpdate = [#keyPath(EAlbum.markUnread): false]
request.resultType = .updatedObjectIDsResultType
do {
let result = try context.execute(request) as? NSBatchUpdateResult
guard let objectIDArray = result?.result as? [NSManagedObjectID] else { return false }
let changes = [NSUpdatedObjectsKey : objectIDArray]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
return true
} catch {
return false
}
}