Master data establishing relationships on an object causes an error in the query results of a fetch controller delegate

(Entity names have been changed to facilitate reading)

Environment: Xcode 7 beta 4. iOS 9.0 is the base SDK, and the deployment target is iOS 8.0. Debugging on a real iPhone 5s device with iOS 8.4.

Here is an interesting Core Data error that I spent the last couple of days debugging / trying to understand.

-

I created an object RecipeEntitythat has some attributes and a many-to-many relationship with an entity IngredientEntityto represent its ingredients. IngredientsEntityhas a corresponding inverse relation to RecipeEntity, both objects have the following properties established in the relationship:

enter image description here

, , , NSFetchedResultsController , NSFetchedResultsControllerDelegate - .

, :

// MARK: - FetchedResultsController Delegate

/*
We're using a nifty solution to fix the issue of multiple Core Data changes occuring at the same time.
See here for an explanation of why and how it works:
http://www.fruitstandsoftware.com/blog/2013/02/19/uitableview-and-nsfetchedresultscontroller-updates-done-right/
*/

private var deletedSectionIndexes = NSMutableIndexSet()
private var insertedSectionIndexes = NSMutableIndexSet()
private var deletedRowIndexPaths = [NSIndexPath]()
private var insertedRowIndexPaths = [NSIndexPath]()
private var updatedRowIndexPaths = [NSIndexPath]()

func controller(controller: NSFetchedResultsController, didChangeObject anObject: NSManagedObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch type {
    case NSFetchedResultsChangeType.Insert:
        if let newIndexPath = newIndexPath {
            if insertedSectionIndexes.containsIndex(newIndexPath.section) {
                // If we've already been told that we're adding a section for this inserted row we skip it since it will handled by the section insertion.
                return
            }
            insertedRowIndexPaths.append(newIndexPath)
        }
    case NSFetchedResultsChangeType.Delete:
        if let indexPath = indexPath {
            if deletedSectionIndexes.containsIndex(indexPath.section) {
                // If we've already been told that we're deleting a section for this deleted row we skip it since it will handled by the section deletion.
                return
            }
            deletedRowIndexPaths.append(indexPath)
        }
    case NSFetchedResultsChangeType.Move:
        if let newIndexPath = newIndexPath {
            if !insertedSectionIndexes.containsIndex(newIndexPath.section) {
                insertedRowIndexPaths.append(newIndexPath)
            }
        }
        if let indexPath = indexPath {
            if !deletedSectionIndexes.containsIndex(indexPath.section) {
                deletedRowIndexPaths.append(indexPath)
            }
        }
    case NSFetchedResultsChangeType.Update:
        if let indexPath = indexPath {
            updatedRowIndexPaths.append(indexPath)
        }
    }
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    switch type {
    case NSFetchedResultsChangeType.Insert:
        insertedSectionIndexes.addIndex(sectionIndex)
    case NSFetchedResultsChangeType.Delete:
        deletedSectionIndexes.addIndex(sectionIndex)
    default:
        break
    }
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    tableView.beginUpdates()

    tableView.deleteSections(deletedSectionIndexes, withRowAnimation: UITableViewRowAnimation.Automatic)
    tableView.insertSections(insertedSectionIndexes, withRowAnimation: UITableViewRowAnimation.Automatic)

    tableView.deleteRowsAtIndexPaths(deletedRowIndexPaths, withRowAnimation: UITableViewRowAnimation.Left)
    tableView.insertRowsAtIndexPaths(insertedRowIndexPaths, withRowAnimation: UITableViewRowAnimation.Right)
    tableView.reloadRowsAtIndexPaths(updatedRowIndexPaths, withRowAnimation: UITableViewRowAnimation.Automatic)

    tableView.endUpdates()

    // Clear the collections so they are ready for their next use.
    insertedSectionIndexes = NSMutableIndexSet()
    deletedSectionIndexes = NSMutableIndexSet()
    deletedRowIndexPaths = [NSIndexPath]()
    insertedRowIndexPaths = [NSIndexPath]()
    updatedRowIndexPaths = [NSIndexPath]()
}

, IngredientEntity, RecipeEntity, :

2015-08-12 14: 43: 34.777 MyApp [9707: 2866157] *** - [MyApp.MyAppTableView _endCellAnimationsWithContext:],/SourceCache/UIKit/UIKit-3347.44.2/UITableView.m:1222
2015-08-12 14: 43: 34.782 MyApp [9707: 2866157] CoreData: : . NSFetchedResultsController -controllerDidChangeContent:. ({length = 2, path = 0 - 0}) userInfo (null)

controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:, NSFetchedResultsChangeType.Move, NSIndexPath indexPath newIndexPath.

, :

// setOfIngredients is an NSSet of IngredientEntity retreived from Core Data.
recipeEntity.ingredients = setOfIngredients

case case ( controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:) :

    case NSFetchedResultsChangeType.Move:
        // If the indexPath and the newIndexPath are the same, then they shouldn't really be deleted and inserted, the row should just be reloaded,
        // otherwise it causes a crash.
        if indexPath != newIndexPath {
            if let newIndexPath = newIndexPath {
                if !insertedSectionIndexes.containsIndex(newIndexPath.section) {
                    insertedRowIndexPaths.append(newIndexPath)
                }
            }
            if let indexPath = indexPath {
                if !deletedSectionIndexes.containsIndex(indexPath.section) {
                    deletedRowIndexPaths.append(indexPath)
                }
            }
        } else if let indexPath = indexPath {
            updatedRowIndexPaths.append(indexPath)
        }

-

:

, , Core Data, controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: .Move, ?

​​Xcode 7 beta 4/Swift 2.0 - ?

, , , , !

, - , , , .

+4
1

, - , . , , .

, indexPath, newIndexPath . .

NSFetchedResultsController.h

The Move object is reported when the changed attribute on the 
object is one of the sort descriptors used in the fetch request. 
An update of the object is assumed in this case, but no separate 
update message is sent to the delegate.
0

All Articles