views:

1002

answers:

3

Edit: a minimal project that exhibits the crash can be downloaded from crashTest. It was created by choosing the "navigation based with core data" project template in XCode and modifying maybe ten lines.

I have ran out of hairs to pull with a crash when a section and two objects are added in one go.

The crash happens at the end of the routine inside the call to [managedObjectContext save:&error].

The crash is an out-of-bound exception for an NSArray:

Serious application error.  Exception was caught during Core Data change processing: *** -[NSCFArray objectAtIndex:]: index (1) beyond bounds (1) with userInfo (null)

Also maybe relevant, when the exception happen, my fetch result controller controllerDidChangeContent: delegate routine is in the call stack. It simply calls my table view endUpdate routine.

I am now running out of ideas. How am I supposed to insert more than one item to a core data store with a table view using sections?

Here is the call stack:

#0  0x901ca4e6 in objc_exception_throw
#1  0x01d86c3b in +[NSException raise:format:arguments:]
#2  0x01d86b9a in +[NSException raise:format:]
#3  0x00072cb9 in _NSArrayRaiseBoundException
#4  0x00010217 in -[NSCFArray objectAtIndex:]
#5  0x002eaaa7 in -[UITableView(_UITableViewPrivate) _endCellAnimationsWithContext:]
#6  0x002def02 in -[UITableView endUpdates]
#7  0x00004863 in -[AirportViewController controllerDidChangeContent:] at AirportViewController.m:463
#8  0x01c43be1 in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:]
#9  0x0001462a in _nsnote_callback
#10 0x01d31005 in _CFXNotificationPostNotification
#11 0x00011ee0 in -[NSNotificationCenter postNotificationName:object:userInfo:]
#12 0x01ba417d in -[NSManagedObjectContext(_NSInternalNotificationHandling) _postObjectsDidChangeNotificationWithUserInfo:]
#13 0x01c03763 in -[NSManagedObjectContext(_NSInternalChangeProcessing) _createAndPostChangeNotification:withDeletions:withUpdates:withRefreshes:]
#14 0x01b885ea in -[NSManagedObjectContext(_NSInternalChangeProcessing) _processRecentChanges:]
#15 0x01bbe728 in -[NSManagedObjectContext save:]
#16 0x000039ea in -[AirportViewController populateAirports] at AirportViewController.m:112

Here is the code to the routine. I apologize because a number of lines are probably irrelevant, but I'd rather err on that side. The crash happens when it calls [context save:&error]:

- (void) insertObjects
{
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];

NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];

// If appropriate, configure the new managed object.
[newManagedObject setValue:@"new airport1" forKey:@"name"];
[newManagedObject setValue:@"???" forKey:@"code"];
[newManagedObject setValue:@"new country" forKey:@"country_name"];

newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:@"new airport2" forKey:@"name"];
[newManagedObject setValue:@"???" forKey:@"code"];
[newManagedObject setValue:@"new country" forKey:@"country_name"];


// Save the context.
NSError *error = nil;
if (![context save:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}
}

Note: the sections are by country_name. Also, the four NSFetchedResultsControllerDelegate routines are as documented and as preset by XCode:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
       atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
    case NSFetchedResultsChangeInsert:
        [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
        break;
// other cases omitted because not occurring in this crash            

}
}


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;

switch(type) {

    case NSFetchedResultsChangeInsert:
        [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;
// other cases omitted because not occurring in this crash            
}
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
+1  A: 

The crash here is in your UITableView's update routine (and its subsequent attempt to animate). You are not getting coherent calls to insertRowsAtIndexPaths:withRowAnimation: and its kin within the beginUpdates/endUpdates block. By "coherent" I mean that the results from routines like numberOfRowsInSection need to change in ways consistent with the inserts and deletes.

Are you calling beginUpdates in controllerWillChangeContent:? Have you implemented the other code detailed in the NSFetchedResultsControllerDelegate docs under "Typical Use?" In particular, do you implement controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: as described? This would be the routine I most suspected given your crash.

Rob Napier
Yes I have. Or rather, XCode has. I haven't changed them from what the project template inserted. (Navigation kind of app with Core Data).
Jean-Denis Muys
I checked the doc, and my 4 `NSFetchedResultsControllerDelegate` routines are literally as given in the doc.
Jean-Denis Muys
I totally simplified my crash case above, and I added the four delegate routines.
Jean-Denis Muys
+2  A: 

It seems to me this is a bug in Cocoa Touch. I may be wrong of course. In any case, I found a work around.

The work around consist of doing nothing in the four delegate routines, but only in that case. I ended up adding a BOOL massUpdate iVar that I set to true before adding the objects, and that I reset to false after the call to save.

In the four delegate routines, I test the massUpdate iVar. If it's true, I do nothing, except in the fourth, where I reload the whole table view.

I get:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
if (massUpdate)
    return;
[self.tableView beginUpdates];
}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
       atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
if (massUpdate)
    return;
    <snip normal implementation>
}


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath
{
if (massUpdate)
    return;
    <snip normal implementation>
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (massUpdate)
    [self.tableView reloadData];
else
    [self.tableView endUpdates];
}
Jean-Denis Muys
A: 

It is not a bug in Cocoa Touch. These delegate methods are being used constantly and they work just fine.

First, you should put a breakpoint on objc_exception_throw and then run it in the debugger. This will cause the code to stop just before the exception occurs and help narrow down where it is happening.

Now, I suspect that the error is not occurring in the delegate methods but because of them. From the stack trace I suspect you have an issue in either your -numberOfSectionsInTableView: method or your -tableView:numberOfRowsInSection: method. I would be very interested to see those.

UPDATE

Seems I must stand corrected. It does appear that you found a bug in the current implementation of Core Data on Cocoa Touch. Fortunately this particular error, while very interesting, is easily avoided.

The issue is with the creation of two Event objects after the NSFetchedResultsController has been created with an empty database. Specifically the creation of the section index seems incapable of handling that situation.

There are a few ways to get around this bug:

  1. Create one Event object at a time. Objects on the other end of a relationship and/or objects in other tables do not seem to impact this bug. You can even create multiple objects in the Event table after the first save but that first save after the NSFetchedResultsController was initialized seems to be causing the issue.
  2. Create the Event objects before you initialize the NSFetchedResultsController. It is the initial update to the section name cache that is causing the error so if the objects exist before the cache then it will not throw.

I have to admit, this is one of the more interesting crashes I have seen and I will be filing a radar against it. I invite you to do the same so that it can be corrected in the (hopefully) next release of the OS.

Marcus S. Zarra
I did break on throwing of exceptions, as you can see from the call stack I posted.Also the delegate methods are vanilla unchanged from those provided by the XCode template. I'd be gladly proven wrong about this being a Cocoa Touch bug. The crash is very easy to replicate from a vanilla core-data navigation-based application:1- create such a projet2- add a string attribute to the sample Event entity provided. For example name it "kind"3- modifiy the `fetchedResultsController` to create sections on `kind` (change the call to `initWithFetchRequest` to pass `@"kind"` instead of `nil`)
Jean-Denis Muys
4- add a vanilla section labeling delegate method: `- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return [[[fetchedResultsController sections] objectAtIndex:section] name]; }`
Jean-Denis Muys
5- modify the `insertNewObject` routine to create 2 events instead of one: `[newManagedObject setValue:@"lunch" forKey:@"kind"]; newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context]; [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"]; [newManagedObject setValue:@"lunch" forKey:@"kind"];`That's it. Launch the app with an empty state, tap the "+" button, watch the crash
Jean-Denis Muys
as requested I have: - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[fetchedResultsController sections] count];}
Jean-Denis Muys
and- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section]; NSUInteger n = [sectionInfo numberOfObjects]; return n;}
Jean-Denis Muys
note these two methods are never called between `save` and the crash
Jean-Denis Muys
you can download such a minimally modified XCode project which shows the crash from the link I added at the top of my question
Jean-Denis Muys
Thanks, I am playing with it now to track down the crash. Should have something today.
Marcus S. Zarra
done. rdar://7757591
Jean-Denis Muys