How to populate a core data store programmatically?
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
:
Serio开发者_如何学编程us 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];
}
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];
}
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.
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:
- 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. - 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.
精彩评论