Reputation: 19862
I'm fighting with my first CoreData application and I stuck with several problems - I feel like I'm doing something really wrong.
On main thread I have a UITableView with NSFetchedResultsController, create with this code:
- (NSFetchedResultsController *)newFetchedResultsControllerWithSearch:(NSString *)searchString
{
NSSortDescriptor *sort = [[[NSSortDescriptor alloc] initWithKey:@"order" ascending:YES] autorelease];
NSArray* sortDescriptors = @[sort];
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"contactId != 1"];
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
// Edit the entity name as appropriate.
NSEntityDescription *callEntity = [NSEntityDescription entityForName:@"ContactDB" inManagedObjectContext:[[AppManager sharedAppManager] managedObjectContext]];
[fetchRequest setEntity:callEntity];
NSMutableArray *predicateArray = [NSMutableArray array];
if(searchString.length)
{
// your search predicate(s) are added to this array
[predicateArray addObject:[NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", searchString]];
// finally add the filter predicate for this view
if(filterPredicate)
{
filterPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:filterPredicate, [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray], nil]];
}
else
{
filterPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray];
}
}
[fetchRequest setPredicate:filterPredicate];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
[fetchRequest setSortDescriptors:sortDescriptors];
// 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:[[AppManager sharedAppManager] managedObjectContext]
sectionNameKeyPath:nil
cacheName:nil];
aFetchedResultsController.delegate = self;
NSError *error = nil;
if (![aFetchedResultsController 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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return aFetchedResultsController;
}
I'm doing some background downloads to update the data which is displayed inside that UITableView. According to informations I found on web I'm creating new MOC to do those changes and I'm merging it later with my main thread MOC.
- (void)managedObjectContextDidSave:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
if(notification.object != [[AppManager sharedAppManager] managedObjectContext]) {
[[[AppManager sharedAppManager] managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}
});
}
For merging delegates I'm using standard Apple functions.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)theIndexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self fetchedResultsController:controller configureCell:[tableView cellForRowAtIndexPath:theIndexPath] atIndexPath:theIndexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView endUpdates];
}
Problem 1: Notification comes through and merges are visible inside UITableView, but problem is filtering doesn't work for merges. As you can see in my filters I'm skipping contactId = 1, but when merges are being done this contact appears inside UITableView.
I'm still not sure if there is a better way to react on changes done to database on another thread - like reloading table view and refetching UITableView data.
Problem 2: When I scroll fast while it's reloading it's crashing with message:
Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x19762a40
That only happens if I delete in background thread and it appears in code responsible for creating a UITableViewCell - it crashes on accessing one of properties.
EDIT
As a workaround I've made:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView endUpdates];
[controller performFetch:nil];
[tableView reloadData];
}
So I'm manually performingFetch and reloadingData to filter entries again.
Here is how I construct CoreData related objects:
- (NSManagedObjectModel *)managedObjectModel
{
if(_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
self.managedObjectModel = [[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL] autorelease];
return _managedObjectModel;
}
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
self.managedObjectContext = [self createManagedObjectContextWithConcurrencyType:NSMainQueueConcurrencyType];
return _managedObjectContext;
}
- (NSManagedObjectContext *) createManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrrencyType
{
NSManagedObjectContext* managedObjectContext = [[[NSManagedObjectContext alloc] initWithConcurrencyType:concurrrencyType] autorelease];
if(concurrrencyType == NSMainQueueConcurrencyType)
{
[managedObjectContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
}
else
{
[managedObjectContext setParentContext:self.managedObjectContext];
}
return managedObjectContext;
}
And here is code for updating contacts:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSManagedObjectContext* context = [[AppManager sharedAppManager] createManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType];
//clear database
NSArray* allContacts = [context fetchObjectsForEntityName:@"ContactDB"];
if([allContacts count] > 0)
{
for (ContactDB* contact in allContacts)
{
[context deleteObject:contact];
}
}
NSArray* responseContacts = responseObject[@"contacts"];
[responseContacts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSMutableDictionary* contactDict = [NSMutableDictionary dictionaryWithDictionary:obj];
ContactDB* contact = [[[ContactDB alloc] initWithDictionary:contactDict andContext:context] autorelease];
contact.order = [NSNumber numberWithInt:[order indexOfObject:contact.contactId]];
}];
[context save:nil];
[[[AppManager sharedAppManager] managedObjectContext] performBlock:^{
[[[AppManager sharedAppManager] managedObjectContext] save:nil];
}];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_LOADING_STOPPED object:self userInfo:nil];
});
});
Upvotes: 1
Views: 1464
Reputation: 46728
If you are creating multiple MOCs, is there a reason you are not using the parent/child design? If you are using parent/child then you do not need to merge changes. That is one of the benefits of parent/child.
From the error, I suspect you are breaking the thread boundary rules. The question is where hence my question about using parent/child.
When you hit this error, where does the break point fire at? What line of code is responsible for triggering that error? What thread is that line of code being fired on?
What does the predicate end up looking like at the end of this? Have you printed the final predicate to console to take a look at it and make sure it is building the way you think it should be? I see a lot of and/or combinations in there.
Did your crash go away with the switch to parent child? I ask because I looked at your code for the merging and it could be suspect. Better to write it as:
- (void)managedObjectContextDidSave:(NSNotification *)notification
{
NSManagedObjectContext *moc = [[AppManaged sharedAppManager] managedObjectContext];
[moc performBlock:^{
[moc mergeChangesFromContextDidSaveNotification:notification];
}];
}
That way you are guaranteed to be on the right queue for the merge.
Upvotes: 1
Reputation: 19862
After 2 days of looking around I prepared a simpler example of my problem from scratch and I found a problem finally. I still don't understand why it was working from loaded database not for updates but I changed:
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"contactId != 1"];
To:
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"contactId != %@", @"1"];
I understand first one is incorrect because it's not NSString and my contactId is NSString, but why it is working when I load app - when core data loads from disk. And for merges and updates NSPredicate with != 1 was not working.
Maybe it will help someone - make sure for predicates you respect type.
Upvotes: 1
Reputation: 451
I'd like to note that you can pass NSNotification
object posted on another thread to mergeChangesFromContextDidSaveNotification
without dispatching.
Also, in the code for updating contacts instead of dispatching you should use performBlock:
method of NSManagedObjectContext
since it's created with NSPrivateQueueConcurrencyType
concurrency type.
So the idea is create NSManagedObjectContext
with concurrency type NSPrivateQueueConcurrencyType
, and set it's parent to your main NSManagedObjectContext
. Then you use main MOC for 'NSFetchedResultsController', and and private MOC for updating data.
[privateMOC performBlock:^{
// update your data
[privateMOC save:NULL]; // you should use NSError though
[privateMOC.parentContext performBlock:^{
[privateMOC.parentContext save:NULL]; // there your NSFetchedResultsController delegate methods should be fired automatically
}];
}];
As you see, there is no need for dispatching. And, you really should use ARC. I'm sure compiler more efficient than we.
Update
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"contactId != 1"];
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] initWithEntityName:@"ContactDB"] autorelease];
if(searchString.length)
{
// your search predicate(s)
NSPredicate *searchTermPredicate = [NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", searchString];
// finally add the filter predicate for this view
filterPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:filterPredicate, searchTermPredicate, nil]];
}
Upvotes: 0