NSFetchedResultsController returning ghost records

1

I have been troubled by a problem for months that I finally need to tackle and put to bed once and for all.

I have a table view fed by an NSFetchedResultsController. This is attached to core data. I use seperate classes to populate the core data from a web service.

Here is the scenario: You load the table up and the NSFetchedResultsController returns 10 rows from the NSManagedObjectContext.

You edit record 1.

You then signal to the database classes that you want to conduct an update. The classes upload the altered record and then demand any records from the web service that were changed since the last update. This result set includes the updated record 1. My class deletes record 1 from the NSManagedObjectContext, and then inserts the new record 1 that it downloaded from the webservice and commits the changes.

The database classes are now complete.

The tableView now needs to update. The NSFetchedResultsController performs a new fetch. The NSManagedObjectContext returns 10 records, including the new record 1 as downloaded from the webservice, remembering that the old record 1 that was uploaded has been deleted.

We now open record 2 or add a new record, anything just access the records in the table.

Now any attempt to ask the NSFetchedResultsController for a list of records (say [self.tableView reloadData] now returns 11 records, the 10 that exist in the NSManagedObjectContext plus the old record 1 that we deleted. I have stepped through the code at this point and put in various NSLogs at this point which check the NSManagedObjectContext and confirm that:

The NSManagedObjectContext contains 10 records. The NSFetchedResultsController contains 11 records.

I have used self.NSFetchedResultsController = nil immediately after the database classes return from having updated the NSManagedObjectContext in an attempt to refresh the FetchedResultsController, and it does a new fetch (after all, it does return 10 records correctly after this point), but it still returns 11 records after you make any attempt to alter or access any of the records.

Does anyone know where this ghost record is coming from? It really does seem to be coming from the NSFetchedResultsController because at no point does the NSManagedObjectContext ever contain 11 records and no fetch command ever returns 11 records.

If you try to open this ghost record you get an assertion failure as the system is trying to fulfil a fault that obviously doesn't exist.

For anyone interested this is how the data is loaded from the web service: The UITableViewController instanciates a class responsible for datastorage called datastoreSync. we then create a background thread, in this background thread we create a new instance of NSManagedObjectContext from the original one and then assign this to the datatoreClass and setup the delegates plus the notification from the NSManagedObjectContext that synchronises with the main thread's NSManagedObjectContext after updates etc.

Once this is complete the datastoreSync class raises a delegate at the end of the process to inform us that the update is complete. In an attempt to resolve the stated problem I have tried invalidating the NSFetchedResultsController at this point and triggering a tableview reload but it has not solved the issue.

As requested here is the code for the NSFetchedResultsController

- (NSFetchedResultsController *)fetchedResultsController
{
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:@"ShiftLog" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];

// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];

// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"logNumber" ascending:NO];
NSSortDescriptor *sortDescriptorProgress = [[NSSortDescriptor alloc] initWithKey:@"pendingUpload" ascending:NO];
NSSortDescriptor *sortDescriptorComplete = [[NSSortDescriptor alloc] initWithKey:@"complete" ascending:YES];
NSArray *sortDescriptors = @[sortDescriptorProgress, sortDescriptorComplete, sortDescriptor];


[fetchRequest setSortDescriptors:sortDescriptors];

// Load an externally derived asset if needed.

NSPredicate *predicate = [self getFilterPredicate];
if(predicate !=nil){
    [fetchRequest setPredicate:predicate];
}

// Remember the request.
_request = fetchRequest;

// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;


if (self.fetchedResultsController.fetchedObjects == nil){
    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
        // Replace this implementation with code to handle the error appropriately.
        // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        DLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
}else{
    DLog(@"Caught a populated fetchresultscontroller");
}
return _fetchedResultsController;

}

As requested here is a copy of the fault that you can get

Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x9b5a7c0 '' * First throw call stack: ( 0 CoreFoundation 0x02ad25e4 exceptionPreprocess + 180 1 libobjc.A.dylib 0x026698b6 objc_exception_throw + 44 2 CoreData 0x0233f33b _PFFaultHandlerLookupRow + 2715 3 CoreData 0x0233e897 -[NSFaultHandler fulfillFault:withContext:forIndex:] + 39 4 CoreData 0x0233e473 _PF_FulfillDeferredFault + 259 5 CoreData 0x0233e2c6 _sharedIMPL_pvfk_core + 70 6 CoreData 0x0234a4d0 _pvfk_7 + 32 7 [projectname] 0x00040170 -[DetailedViewController configureView] + 336 8 [projectname] 0x0003dfcb -[DetailedViewController setShiftLogObject:] + 331 9 [projectname] 0x000063ac -[MasterViewController alertView:clickedButtonAtIndex:] + 828 10 UIKit 0x01349ef3 -[UIAlertView(Private) modalItem:tappedButtonAtIndex:] + 67 11 UIKit 0x01417785 -[_UIModalItemsCoordinator _notifyDelegateModalItem:tappedButtonAtIndex:] + 180 12 UIKit 0x00f7305b -[_UIModalItemAlertContentView tableView:didSelectRowAtIndexPath:] + 380 13 UIKit 0x00f4a7b1 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1513 14 UIKit 0x00f4a924 -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 279 15 UIKit 0x00f4e908 __38-[UITableView touchesEnded:withEvent:]_block_invoke + 43 16 UIKit 0x00e85183 ___afterCACommitHandler_block_invoke + 15 17 UIKit 0x00e8512e _applyBlockToCFArrayCopiedToStack + 403 18 UIKit 0x00e84f5a _afterCACommitHandler + 532 19 CoreFoundation 0x02a9a4ce __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION + 30 20 CoreFoundation 0x02a9a41f __CFRunLoopDoObservers + 399 21 CoreFoundation 0x02a78344 __CFRunLoopRun + 1076 22 CoreFoundation 0x02a77ac3 CFRunLoopRunSpecific + 467 23 CoreFoundation 0x02a778db CFRunLoopRunInMode + 123 24 GraphicsServices 0x034e09e2 GSEventRunModal + 192 25 GraphicsServices 0x034e0809 GSEventRun + 104 26 UIKit 0x00e68d3b UIApplicationMain + 1225 27 [projectname] 0x0000258d main + 141 28 libdyld.dylib 0x0397970d start + 1 ) libc++abi.dylib: terminating with uncaught exception of type _NSCoreDataException

ios
objective-c
core-data
asked on Stack Overflow Nov 20, 2013 by Craig Moore • edited Feb 27, 2015 by Drux

1 Answer

0

Well this is a strange one but I have got it doing what I need now. When I start the background task updating I pass it its own NSManagedObjectContext. I set my main thread as a subscriber to the save event in this NSManagedObjectContext so that I can update the main threads MOC when the background MOC actions a save.

- (void)mergeChanges:(NSNotification *)notification
{
DLog(@"Merge changes has begun");

// Merge changes into the main context on the main thread
NSManagedObjectContext *incommingContext = [notification object];

if (incommingContext != self.managedObjectContext){
    dispatch_sync(dispatch_get_main_queue(), ^{
        [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
    });
}
//[self compareMOCs];
}

When you action NSManagedObjectContext rollback the NSManagedObjectContext does not roll back (because it is saved) but the NSFetchedResultsController does and shows up the two deleted records.

To work around this I am having to save my main threads NSManagedObjectContext in the main thread after a save merge as so:

- (void)mergeChanges:(NSNotification *)notification
{
DLog(@"Merge changes has begun");

// Merge changes into the main context on the main thread
NSManagedObjectContext *incommingContext = [notification object];

if (incommingContext != self.managedObjectContext){
    dispatch_sync(dispatch_get_main_queue(), ^{
        [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];

        // no idea why I have to do this but if I do not do this any
        // attempt to rollback the MOC will cause the NSFetchedResultsController to pull out
        // ghost records.
        [self.managedObjectContext save:nil];

    });
}
//[self compareMOCs];
}

According to Apple this should not be required as the notification is only fired after a save event on the background MOC. I cannot explain why this is happening, I can only tell you that it is happening and this extra save has fixed it.

answered on Stack Overflow Nov 21, 2013 by Craig Moore

User contributions licensed under CC BY-SA 3.0