Problems saving NSManagedObjects on a background Context - ios

I've been struggling with this for days. I'll appreciate any help.
I have a Location NSManagedObject and an Image NSManagedObject, they have one-to-many relationship, i.e., one location has many images.
I have 2 screens, in the first one the user adds locations on the view context and they get added and retrieved without problems.
Now, in the second screen, I want to retrieve images based on the location selected in the first screen, then display the images in a Collection View. The images are first retrieved from flickr, then saved in the DB.
I want to save and retrieve images on a background context and this causes me a lot of problems.
When I try to save every image retrieved from flickr I get a warning stating that there is a dangling object and the relationship can' be established:
This is my saving code:
func saveImagesToDb () {
//Store the image in the DB along with its location on the background thread
if (doesImageExist()){
dataController.backgroundContext.perform {
for downloadedImage in self.downloadedImages {
print ("saving to context")
let imageOnMainContext = Image (context: self.dataController.viewContext)
let imageManagedObjectId = imageOnMainContext.objectID
let imageOnBackgroundContext = self.dataController.backgroundContext.object(with: imageManagedObjectId) as! Image
let locationObjectId = self.imagesLocation.objectID
let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
let imageData = NSData (data: downloadedImage.jpegData(compressionQuality: 0.5)!)
imageOnBackgroundContext.image = imageData as Data
imageOnBackgroundContext.location = locationOnBackgroundContext
try? self.dataController.backgroundContext.save ()
}
}
}
}
As you can see in the code above I'm building NSManagedObject on the background context based on the ID retrieved from those on the view context. Every time saveImagesToDb is called I get the warning, so what's the problem?
In spite of the warning above, when I retrieve the data through a FetchedResultsController (which works on the background context). The Collection View sometimes view the images just fine and sometimes I get this error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (4) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
Here are some code snippets that are related to setting up the FetchedResultsController and updating the Collection View based on changes in the context or in the FetchedResultsController.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let imagesCount = fetchedResultsController.fetchedObjects?.count else {return 0}
return imagesCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print ("cell data")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as! ImageCell
//cell.placeImage.image = UIImage (named: "placeholder")
let imageObject = fetchedResultsController.object(at: indexPath)
let imageData = imageObject.image
let uiImage = UIImage (data: imageData!)
cell.placeImage.image = uiImage
return cell
}
func setUpFetchedResultsController () {
print ("setting up controller")
//Build a request for the Image ManagedObject
let fetchRequest : NSFetchRequest <Image> = Image.fetchRequest()
//Fetch the images only related to the images location
let locationObjectId = self.imagesLocation.objectID
let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
let predicate = NSPredicate (format: "location == %#", locationOnBackgroundContext)
fetchRequest.predicate = predicate
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "location", ascending: true)]
fetchedResultsController = NSFetchedResultsController (fetchRequest: fetchRequest, managedObjectContext: dataController.backgroundContext, sectionNameKeyPath: nil, cacheName: "\(latLongString) images")
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch ()
} catch {
fatalError("couldn't retrive images for the selected location")
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
print ("object info changed in fecthed controller")
switch type {
case .insert:
print ("insert")
DispatchQueue.main.async {
print ("calling section items")
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.insertItems(at: [newIndexPath!])
}
break
case .delete:
print ("delete")
DispatchQueue.main.async {
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.deleteItems(at: [indexPath!])
}
break
case .update:
print ("update")
DispatchQueue.main.async {
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.reloadItems(at: [indexPath!])
}
break
case .move:
print ("move")
DispatchQueue.main.async {
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.moveItem(at: indexPath!, to: newIndexPath!)
}
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
print ("section info changed in fecthed controller")
let indexSet = IndexSet(integer: sectionIndex)
switch type {
case .insert:
self.collectionView!.numberOfItems(inSection: 0)
collectionView.insertSections(indexSet)
break
case .delete:
self.collectionView!.numberOfItems(inSection: 0)
collectionView.deleteSections(indexSet)
case .update, .move:
fatalError("Invalid change type in controller(_:didChange:atSectionIndex:for:). Only .insert or .delete should be possible.")
}
}
func addSaveNotificationObserver() {
removeSaveNotificationObserver()
print ("context onbserver notified")
saveObserverToken = NotificationCenter.default.addObserver(forName: .NSManagedObjectContextObjectsDidChange, object: dataController?.backgroundContext, queue: nil, using: handleSaveNotification(notification:))
}
func removeSaveNotificationObserver() {
if let token = saveObserverToken {
NotificationCenter.default.removeObserver(token)
}
}
func handleSaveNotification(notification:Notification) {
DispatchQueue.main.async {
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.reloadData()
}
}
What am I doing wrong? I'll appreciate any help.

I cannot tell you what the problem is with 1), but I think 2) is not (just) a problem with the database.
The error your are getting usually happens when you add or remove items/sections to a collectionview, but when numberOfItemsInSection is called afterwards the numbers don't add up. Example: you have 5 items and add 2, but then numberOfItemsInSection is called and returns 6, which creates the inconsistency.
In your case my guess would be that you add items with collectionView.insertItems(), but this line returns 0 afterwards:
guard let imagesCount = fetchedResultsController.fetchedObjects?.count else {return 0}
What also confused me in your code are these parts:
DispatchQueue.main.async {
print ("calling section items")
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.insertItems(at: [newIndexPath!])
}
You are requesting the number of items there, but you don't actually do anything with the result of the function. Is there a reason for that?
Even though I don't know what the CoreData issue is I would advise you to not access the DB in the tableview delegate methods, but to have an array of items that is fetched once and is updated only when the db content changes. That is probably more performant and a lot easier to maintain.

You have a common problem with UICollectionView inconsistency during batching update.
If you perform deletion/adding new items in the incorrect order UICollectionView might crash.
This problem has 2 typical solutions:
use -reloadData() instead of batch updates.
use third party libraries with safe implementation of batch update. Smth like this https://github.com/badoo/ios-collection-batch-updates

The problem is NSFetchedResultsController should only use a main thread NSManagedObjectContext.
Solution: create two NSManagedObjectContext objects, one in the main thread for NSFetchedResultsController and one in the background thread for performing the data writing.
let writeContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let readContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let fetchedController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: readContext, sectionNameKeyPath: nil, cacheName: nil)
writeContext.parent = readContext
UICollectionView will be updated properly once the data is saved in the writeContext with the following chain:
writeContext(background thread ) -> readContext(main thread) -> NSFetchedResultsController (main thread) -> UICollectionView (main thread)

I would like to thank Robin Bork, Eugene El, and meim for their answers.
I could finally solve both issues.
For the CollectionView problem, I felt like I was updating it too many times, as you can see in the code, I used to update it in two FetchedResultsController delegate methods, and also through an observer that observes any changes on the context. So I removed all of that and just used this method:
func controllerWillChangeContent(_ controller:
NSFetchedResultsController<NSFetchRequestResult>) {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
In addition to that, CollectionView has a bug in maintaining the items count in a section sometimes as Eugene El mentioned. So, I just used reloadData to update its items and that worked well, I removed the usage of any method that adjusts its items item by item like inserting an item at a specific IndexPath.
For the dangling object problem. As you can see from the code, I had a Location object and an Image object. My location object was already filled with a location and it was coming from view context, so I just needed to create a corresponding object from it using its ID (as you see in the code in the question).
The problem was in the image object, I was creating an object on the view context (which contains no data inserted), get its ID, then build a corresponding object on the background context. After reading about this error and thinking about my code, I thought that the reason maybe because the Image object on the view context didn't contain any data. So, I removed the code that creates that object on the view context and created a one directly on the background context and used it as in the code below, and it worked!
func saveImagesToDb () {
//Store the image in the DB along with its location on the background thread
dataController.backgroundContext.perform {
for downloadedImage in self.downloadedImages {
let imageOnBackgroundContext = Image (context: self.dataController.backgroundContext)
//imagesLocation is on the view context
let locationObjectId = self.imagesLocation.objectID
let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
let imageData = NSData (data: downloadedImage.jpegData(compressionQuality: 0.5)!)
imageOnBackgroundContext.image = imageData as Data
imageOnBackgroundContext.location = locationOnBackgroundContext
guard (try? self.dataController.backgroundContext.save ()) != nil else {
self.showAlert("Saving Error", "Couldn't store images in Database")
return
}
}
}
}
If anyone has another thought different from what I said about why the first method that first creates an empty Image object on the view context, then creates a corresponding one on the background context didn't work, please let us know.

Related

Delete record in Core Data with CollectionView

I would like delete a record in the Core Data when you tap on a close button in your CollectionView Cell. I made a UIButton in the Collection View Cell controller with an extension in the CollectionView Controller file.
The indexPath give a number and I made the let deleteCellNumber, but I received the error:
'Cannot convert value of type 'IndexPath.Element (aka 'Int') to expected argument type 'NSManagementObject'
extension soundboardVC: SoundboardCellDelegate {
func delete(cell: soundboardCellVC) {
if let indexPath = collectionView?.indexPath(for: cell) {
let soundRequest:NSFetchRequest<Soundboard> = Soundboard.fetchRequest()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let deleteCellNumber = indexPath[1]
context.delete(deleteCellNumber)
(UIApplication.shared.delegate as! AppDelegate).saveContext()
do {
soundBoardData = try managedObjectContext.fetch(soundRequest)
} catch {
print("Fetching Failed")
}
}
}
}
The error is very clear:
context.delete(... expects an NSManagedObject instance but you pass an integer (by the way indexPath[1] is a pretty spooky but valid way to get the item / row value).
Assuming you have a data source array soundBoardData the usual way to delete Core Data objects in a collection or table view is
extension soundboardVC: SoundboardCellDelegate {
func delete(cell: soundboardCellVC) {
if let indexPath = collectionView?.indexPath(for: cell) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let itemToDelete = soundBoardData[indexPath.item]
soundBoardData.remove(at: indexPath.item)
context.delete(itemToDelete)
collectionView!.deleteItems(at: indexPath)
appDelegate.saveContext()
}
}
}
Don't refetch the data, delete in the item in the data source array, then delete the item in the managed object context, remove the row from the collection view (with animation) and save the context.

Using NSFetchedResultsController w/NSBatchDeleteRequest

I'm having an issue getting accurate updates from an NSFetchedResultsControllerDelegate after using an NSBatchDeleteRequest, the changes come through as an update type and not the delete type on the didChange delegate method. Is there a way to get the changes to come in as a delete? (I have different scenarios for delete vs update).
The delete request:
(uses a private context supplied by the caller)
fileprivate class func deletePeopleWith(ids: Set<Int>, usingContext context: NSManagedObjectContext) {
let fr: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Person")
var predicates: [NSPredicate] = []
ids.forEach {
predicates.append(NSPredicate(format: "id = %ld", $0))
}
fr.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
let dr = NSBatchDeleteRequest(fetchRequest: fr)
dr.affectedStores = context.parent?.persistentStoreCoordinator?.persistentStores
do {
try context.parent?.execute(dr)
context.parent?.refreshAllObjects()
print("Deleting person")
} catch let error as NSError {
let desc = "Could not delete person with batch delete request, with error: \(error.localizedDescription)"
debugPrint(desc)
}
}
Results in:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
DispatchQueue.main.async {
let changeDesc = "Have change with indexPath of: \(indexPath), new index path of: \(newIndexPath) for object: \(anObject)"
var changeType = ""
switch type {
case .delete:
changeType = "delete"
case .insert:
changeType = "insert"
case .move:
changeType = "move"
case .update:
changeType = "update"
}
print(changeDesc)
print(changeType)
}
}
Printing:
Have change with indexPath of: Optional([0, 0]), new index path of: Optional([0, 0]) for object: *my core data object*
update
From Apple's documentation:
Batch deletes run faster than deleting the Core Data entities yourself in code because they operate in the persistent store itself, at the SQL level. As part of this difference, the changes enacted on the persistent store are not reflected in the objects that are currently in memory.
After a batch delete has been executed, remove any objects in memory that have been deleted from the persistent store.
See the Updating Your Application After Execution section for handling the objects deleted by NSBatchDeleteRequest.
Here is the sample, works for me. But i didn't find how to update deletion with animation, cause NSFetchedResultsControllerDelegate didn't fire.
#IBAction func didPressDelete(_ sender: UIBarButtonItem) {
let managedObjectContext = persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: Record.self))
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
do {
// delete on persistance store level
try managedObjectContext.execute(deleteRequest)
// reload the whole context
managedObjectContext.reset()
try self.fetchedResultsController.performFetch()
tableV.reloadData()
} catch {
print("🍓")
}
}
Thanks.
Since NSBatchDeleteRequest deletes the managed objects at the persistentStore level (on disk), the most effective way to update the current context (in-memory) is by calling refreshAllObjects() on the context like so:
func batchDelete() {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
let context = appDelegate.persistentContainer.viewContext
if let request = self.fetchedResultsController.fetchRequest as? NSFetchRequest<NSFetchRequestResult> {
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: request)
do {
try context.execute(batchDeleteRequest) // performs the deletion
appDelegate.saveContext()
context.refreshAllObjects() // updates the fetchedResultsController
} catch {
print("Batch deletion failed.")
}
}
}
}
No need for refetching all the objects or tableView.reloadData()!

Is performing fetch for NSFetchedResultsController on a background thread a good idea?

When a user runs the app, he first has to login. If the login is successful, a function called setUpSaving is triggered. In setUpSaving, as I wrote in the commented part of the code, ( "some firebase stuff happens, grabs all the messages, and for each message, one by one, it goes through createMessageWithText and inserts the message into core data, eventually saving it. "
So, my problem is here. Let's assume that a user has 20 000 messages associated with someone he is messaging. When he goes to the ChatLogController, all the messages must be present for the user. The way I did that is by using NSFetchedResultsController perform fetch to load all the messages that were saved in the login controller. My problem with that is that all of this is on the main queue, and sometimes, that has frozen the UI for quite some time. My code for chatLogController is below (not all of it, just the fetchedResultsController part and the collectionView), I was wondering how I could perform fetch on a background queue, yet still update the collectionView on the main queue.
func setUpSaving() {
///////some firebase stuff happens, grabs all the messages, and for each message, one by one, it goes through createMessageWithText and inserts the message into core data,
somewhere along the line context.save() gets triggered saving the message.
}
private func createMessageWithText(text: String, friend: Friend, context: NSManagedObjectContext, date: NSDate, isSender: Bool = false, sentStatus: String, fromID: String) -> Mesages {
let message = NSEntityDescription.insertNewObjectForEntityForName("Mesages", inManagedObjectContext: context) as! Mesages
message.user = friend
message.text = text
message.timestamp = date
message.isSender = isSender
message.fromID = fromID
message.status = sentStatus
return message
}
}
lazy var fetchedResultsControler: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Mesages")
fetchRequest.fetchBatchSize = 20
fetchRequest.includesPendingChanges = false
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
fetchRequest.predicate = NSPredicate(format: "user.id = %#", self.friend!.id!)
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
override func viewDidLoad() {
super.viewDidLoad()
NavigationItems()
revealStatus()
do {
try fetchedResultsControler.performFetch()
} catch let err {
print(err)
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) ->
Int {
if let count = fetchedResultsControler.sections?[0].numberOfObjects {
return count
}
return 0
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAtIndexPath: indexPath) as! JSQMessagesCollectionViewCell
return cell
}
No, you should not fetch an FRC that will drive the UI on a background thread.
Your issue is the idea behind loading 20,000 items after login. No-one can or will view that number of messages on a mobile device. You might want to get 20 after login, and the total count, but that's it.
When the user goes to view messages you should load pages, of perhaps 20 to 100 messages, on demand. In this way you don't use resources the user hasn't asked you to use or take any processing time to do it.
Search should also be done by a server.

CoreData with NSFetchResultController :Invalid update with with a property in fetchObjects

I have a serious problem reported by Xcode :
1. I have a NSFetchResultsController
var fetchedResultsController: NSFetchedResultsController {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest = NSFetchRequest()
// Edit the entity name as appropriate.
let entity = NSEntityDescription.entityForName("Choice", inManagedObjectContext: NSManagedObjectContext.MR_defaultContext())
fetchRequest.entity = entity
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 10
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "id", ascending: false)
let sortDescriptors = [sortDescriptor]
fetchRequest.sortDescriptors = sortDescriptors
//NSPredicate
let predicate = NSPredicate(format: "decision.id = %#", decision.id!)
fetchRequest.predicate = predicate
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: NSManagedObjectContext.MR_defaultContext(), sectionNameKeyPath: nil, cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
do{
try _fetchedResultsController?.performFetch()
}catch let error as NSError {
print(error)
}
return _fetchedResultsController!
}
2. And I have a button do add action:
#IBAction func didTouchAddChoiceButton(sender: UIButton) {
let choice = Choice.MR_createEntity() as! Choice
choice.id = GDUtils.CMUUID()
choice.decision = decision
NSManagedObjectContext.MR_defaultContext().MR_saveToPersistentStoreAndWait()
}
3. After adding this Entity. I have a controller to handle updating tableView like this
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch(type){
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Top)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
tableView.cellForRowAtIndexPath(indexPath!)
case .Move:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Left)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
}
}
4.But the problem happened : every time I try to change a property of an entity from fetchedObjects :
let chosenChoice = fetchedResultsController.objectAtIndexPath(currentIndextPath!) as! Choice
chosenChoice.name = tableCell.choiceName.text
I got this message :
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
Can anyone help me to figure out what happened ?
There's a bug in NSFetchedResultsController on iOS8, where FRC calls .Insert instead of .Update. I solved it the way, that I'm reloading the table completely, when .Insert is called on iOS8.
case .Insert:
guard let newIndexPath = newIndexPath else { return }
// iOS8 bug when FRC calls insert instead of Update
if #available(iOS 9, *) {
// insert item normally
} else {
// reload everything
}
Make sure that the managedOjectContext you assigned to NSFetchResultsController is same where you created new Choice NSManagedObject.
ADDED
in "case .Update" code gives you cell of tableview. and you need to update tableView cell data for not getting that error. atleast change/(pretend change) sth in cell.
cell = tableView.cellForRowAtIndexPath(indexPath!)
cell.choiceName.text = "empty"

UISearchDisplayController animate reloadData

I've been reading all the documentation about UISearchDisplayController and its delegate but I can't find any way to animate the table view when the search criteria change.
I'm using these two methods :
They both return YES but still can't find any way to do something similar to :
I don't know if it's important but I'm using an NSfetchedResultsController to populate the UITableView in the UISearchDisplayController
That's it thanks !
When the search string or scope has changed, you assign a new fetch request for the fetched results controller and therefore have to call performFetch to get a new result set. performFetch resets the state of the controller, and it does not trigger any of the FRC delegate methods.
So the table view has to be updated "manually" after changing the fetch request. In most sample programs, this is done by
calling reloadData on the search table view, or
returning YES from shouldReloadTableForSearchString or shouldReloadTableForSearchScope.
The effect is the same: The search table view is reloaded without animation.
I don't think there is any built-in/easy method to animate the table view update when the search predicate changes. However, you could try the following (this is just an idea, I did not actually try this myself):
Before changing the fetch request, make a copy of the old result set:
NSArray *oldList = [[fetchedResultsController fetchedObjects] copy];
Assign the new fetch request to the FRC and call performFetch.
Get the new result set:
NSArray *newList = [fetchedResultsController fetchedObjects];
Do not call reloadData on the search table view.
Compare oldList and newList to detect new and removed objects. Call insertRowsAtIndexPaths for the new objects and deleteRowsAtIndexPaths for the removed objects. This can be done by traversing both lists in parallel.
Return NO from shouldReloadTableForSearchString or shouldReloadTableForSearchScope.
As I said, this is just an idea but I think it could work.
I know this question is old, but I recently faced this and wanted a solution that would work regardless of how the NSFetchedResultsController was initialized. It's based on #martin-r's answer above.
Here's the corresponding gist: https://gist.github.com/stephanecopin/4ad7ed723f9857d96a777d0e7b45d676
import CoreData
extension NSFetchedResultsController {
var predicate: NSPredicate? {
get {
return self.fetchRequest.predicate
}
set {
try! self.setPredicate(newValue)
}
}
var sortDescriptors: [NSSortDescriptor]? {
get {
return self.fetchRequest.sortDescriptors
}
set {
try! self.setSortDescriptors(newValue)
}
}
func setPredicate(predicate: NSPredicate?) throws {
try self.setPredicate(predicate, sortDescriptors: self.sortDescriptors)
}
func setSortDescriptors(sortDescriptors: [NSSortDescriptor]?) throws {
try self.setPredicate(self.predicate, sortDescriptors: sortDescriptors)
}
func setPredicate(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) throws {
func updateProperties() throws {
if let cacheName = cacheName {
NSFetchedResultsController.deleteCacheWithName(cacheName)
}
self.fetchRequest.predicate = predicate
self.fetchRequest.sortDescriptors = sortDescriptors
try self.performFetch()
}
guard let delegate = self.delegate else {
try updateProperties()
return
}
let previousSections = self.sections ?? []
let previousSectionsCount = previousSections.count
var previousObjects = Set(self.fetchedObjects as? [NSManagedObject] ?? [])
var previousIndexPaths: [NSManagedObject: NSIndexPath] = [:]
previousObjects.forEach {
previousIndexPaths[$0] = self.indexPathForObject($0)
}
try updateProperties()
let newSections = self.sections ?? []
let newSectionsCount = newSections.count
var newObjects = Set(self.fetchedObjects as? [NSManagedObject] ?? [])
var newIndexPaths: [NSManagedObject: NSIndexPath] = [:]
newObjects.forEach {
newIndexPaths[$0] = self.indexPathForObject($0)
}
let updatedObjects = newObjects.intersect(previousObjects)
previousObjects.subtractInPlace(updatedObjects)
newObjects.subtractInPlace(updatedObjects)
var moves: [(object: NSManagedObject, fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath)] = []
updatedObjects.forEach { updatedObject in
if let previousIndexPath = previousIndexPaths[updatedObject],
let newIndexPath = newIndexPaths[updatedObject]
{
if previousIndexPath != newIndexPath {
moves.append((updatedObject, previousIndexPath, newIndexPath))
}
}
}
if moves.isEmpty && previousObjects.isEmpty && newObjects.isEmpty {
// Nothing really changed
return
}
delegate.controllerWillChangeContent?(self)
moves.forEach {
delegate.controller?(self, didChangeObject: $0.object, atIndexPath: $0.fromIndexPath, forChangeType: .Move, newIndexPath: $0.toIndexPath)
}
let sectionDifference = newSectionsCount - previousSectionsCount
if sectionDifference < 0 {
(newSectionsCount..<previousSectionsCount).forEach {
delegate.controller?(self, didChangeSection: previousSections[$0], atIndex: $0, forChangeType: .Delete)
}
} else if sectionDifference > 0 {
(previousSectionsCount..<newSectionsCount).forEach {
delegate.controller?(self, didChangeSection: newSections[$0], atIndex: $0, forChangeType: .Insert)
}
}
previousObjects.forEach {
delegate.controller?(self, didChangeObject: $0, atIndexPath: previousIndexPaths[$0], forChangeType: .Delete, newIndexPath: nil)
}
newObjects.forEach {
delegate.controller?(self, didChangeObject: $0, atIndexPath: nil, forChangeType: .Insert, newIndexPath: newIndexPaths[$0])
}
delegate.controllerDidChangeContent?(self)
}
}
It exposes 2 properties on the NSFetchedResultsController, predicate and sortDescriptors which mirrors those found in the fetchRequest.
When either of these properties are set, the controller will automatically compute changes the changes and send them through the delegate, so hopefully there is no major code changes.
If you don't want animations, you can still directly set predicate or sortDescriptors on the fetchRequest itself.

Resources