NSFetchedResultsController - how to handle changing Core Data source? - ios

I'm using Core Data with MagicalRecord and NSFetchedResultsController to display data in a table view. Everything works fine, now I have to change the underlying sqlite-file underneath which leads to a crash.
I'm creating the FRC the following way:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"CDPerson"];
NSSortDescriptor *sortByNameDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"lastName" ascending:YES];
request.sortDescriptors = #[sortByNameDescriptor];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:[NSManagedObjectContext MR_contextForCurrentThread] sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
[_fetchedResultsController performFetch:nil];
When changing the sqlite-file, I'm doing:
[MagicalRecord cleanup];
// delete old sqlite file
// copy new sqlite file
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:SQLITE_FILENAME];
What do I have to do with my FRC to have it take the new storage? Only create a new one seems not enough as I get a crash in
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[[self tableView] endUpdates];
}
How can I achieve this?
EDIT
#question from flexaddicted: The error I'm getting in endUpdates is Assertion failure in -[UITableView _endCellAnimationsWithContext:]
#question from Exploring: you can imagine that I've got two sqlite files which I'm exchanging - both with the same tables, but different content. The FRC shows the content of the first file, now I'd like cleanUp MagicalRecord, let it point to the other store and 'refresh' the FRC.

You basically need to tear everything down and restart. Once you have recreated the Core Data stack you need to recreate the FRC with the new context and reloadData on the table view (you can't just reload some rows because the backing data has completely changed).

Related

How to delete a row from core data and table view

I am getting an error “An NSManagedObjectContext cannot delete objects in other contexts” while trying to delete an object from the core data.
i am using the below code in the load after the normal core data fetch
NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:appDelegate.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
controller.delegate=self;
BOOL success = [controller performFetch:&error];
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObject *managedObject = [controller objectAtIndexPath:indexPath];
//(Alert view)
}
In the alert view clickedButtonAtIndex function , i am trying to delete that particular managed object from the appDelegate.managedObjectContext , but an error “An NSManagedObjectContext cannot delete objects in other contexts” occurs in the line
[appDelegate.managedObjectContext deleteObject:managedObject];
i tried with other managed object context ,
NSManagedObjectContext *context= [controller managedObjectContext];
but getting the same error.
Can anyone please tell why this error occurs and a solution to delete a particular row from the table view and the core data?
You are using a different context to the one the object is from (exactly what the error says), change the delete line to
[managedObject.managedObjectContext deleteObject:managedObject];
Use the same NSManagedContext to delete that's used to fetched the objects, and in the same thread also.

NSFetchedResultsControllers in different view controllers updating each other?

Ok I get more and more general with this question, since I've noticed several lags in my app due to this. I've noticed the problem with reordering, but it happens in other places as well. I have a CoreDataViewController class which all my table view controllers subclass. And in this class I basically have all NSFetchedResultsController delegate methods as they are in the apple docs.
Then I've tried to find out how often changes are noticed by this NSFetchedResultsController to find out where my time lag is:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
if(self.suspendAutomaticTrackingOfChangesInManagedObjectContext) return;
NSLog(#"ControllerDidChangeContent");
TICK;
[self.tableView endUpdates];
TOCK;
}
So for example in my view controller A, I have this fetch request (called from viewDidLoad):
- (void)setupFetchedResultsController
{
//NSError *error = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"SpendingCategory"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"position" ascending:YES]];
//[self.mainCategory.managedObjectContext executeFetchRequest:request error:&error];
request.predicate = [NSPredicate predicateWithFormat:#"belongsToMainCategory = %#", self.mainCategory];
self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
managedObjectContext:self.mainCategory.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}
If I change attributes for my objects in this view controller, my log prints out only once the comment "ControllerDidChangeContent" as it should. And is as fast as expected. And I mean really only a simple attribute change, just changing some number or string etc. such as:
spendingCategory.name = #"Hello world";
If however I've already accessed another view controller which setups its NSFetchedResultsController as well in viewDidLoad, my log is printed out twice. Here is the second NSFetchedResultsController:
- (void)setupFetchedResultsController
{
self.managedObjectContext = ((AppDelegate *)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"SpendingCategory"];
NSSortDescriptor *mainCatPosition = [[NSSortDescriptor alloc]
initWithKey:#"belongsToMainCategory.position" ascending:YES];
NSSortDescriptor *spendingCatPosition = [[NSSortDescriptor alloc]
initWithKey:#"position" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObjects:mainCatPosition,spendingCatPosition,nil];
request.predicate = [NSPredicate predicateWithFormat:#"liveBudget = %#", [NSNumber numberWithBool:YES]];
self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"belongsToMainCategory.position"
cacheName:#"LiveBudget"];
}
My before mentioned simple attribute change takes way longer. And that's because now my log prints out twice (!) ControllerDidChangeContent. The first TICK-TOCK is still as fast as before, but the second takes over one second. And I guess this is because I have two NSFetchedResultsController watching the same entity.
Question:
I still don't quite understand why they influence each other? I mean ok I update an attribute in one view controller so the other one should of course notice this change, but why are two didChangeContent triggered?
Question:
How can I avoid that? Or how can I improve that?
I assume it is better to check why the 1[sec] halt before turning to this solution.
myself, I have not experienced that delay when using multiple FRC's on the same context so my guess is the problem lie elsewhere.
as for your questions:
The FRC is listening for 'NSManagedObjectContextObjectsDidChangeNotification' so all FRC's listening on the same context on the same objects will trigger their delegate's methods when these objects are changed. Hence, it is clear why your log is printed twice (2 different FRC's changed their content)
1[sec] is a long time for blocking the main thread and should be addressed (check with instruments what can be done). As I mentioned, there is no way to avoid that when using FRC's on the same context, but you can create a child context for each VC you stem an make the FRC listen to that child context only, then changes will only be visible to other FRC when you save down the context chain. as long as your VC stack is not very deep this should not be a real problem. In any case you want your other VC's to get updated with changes as they update the tableview to give a smooth user experience, so if the update take too long you should see why.

efficiently display 100,000 items using Core Data

I am using a NSFetchResultsController to display 100,000 + records in a UITableView. This works but it is SLOW, especially on an iPad 1. It can take 7 seconds to load which is torture for my users.
I'd also like to be able to use sections but this adds at least another 3 seconds onto the laod time.
Here is my NSFetchResultsController:
- (NSFetchedResultsController *)fetchedResultsController {
if (self.clientsController != nil) {
return self.clientsController;
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Client" inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];
[request setPredicate:[NSPredicate predicateWithFormat:#"ManufacturerID==%#", self.manufacturerID]];
[request setFetchBatchSize:25];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"UDF1" ascending:YES];
NSSortDescriptor *sort2= [[NSSortDescriptor alloc] initWithKey:#"Name" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObjects:sort, sort2,nil]];
NSArray *propertiesToFetch = [[NSArray alloc] initWithObjects:#"Name", #"ManufacturerID",#"CustomerNumber",#"City", #"StateProvince",#"PostalCode",#"UDF1",#"UDF2", nil];
[request setPropertiesToFetch:propertiesToFetch];
self.clientsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil
cacheName:nil];
return self.clientsController;
}
I have an index on ManufacturerID which is used in my NSPredicate. This seems like a pretty basic NSFetchRequest - anything I can do to speed this up? Or have I just hit a limitation? I must be missing something.
First: you can use the NSFetchedResultsController's cache to speed up display after the first fetch. This should quickly go down to a fraction of a second.
Second: you can try to display the only the first screenful and then fetch the rest in the background. I do this in the following way:
When the view appears, check if you have the first page cache.
If not, I fetch the first page. You can accomplish this by setting the fetch request's fetchLimit.
In case you are using sections, do two quick fetches to determine the first section headers and records.
Populate a second fetched results controller with your long fetch in a background thread.
You can either create a child context and use performBlock: or
use dispatch_async().
Assign the second FRC to the table view and call reloadData.
This worked quite well in one of my recent projects with > 200K records.
I know the answer #Mundi provided is accepted, but I've tried implementing it and ran into problems. Specifically the objects created by the second FRC will be based on the other thread's ManagedObjectContext. Since these objects are not thread safe and belong to their own MOC on the other thread, the solution I found was to fault the objects as they are being loaded. So in cellForRowAtIndexPath I added this line:
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
object = (TapCellar *)[self.managedObjectContext existingObjectWithID:[object objectID] error:nil];
Then you have an object for the correct thread you are in. One further caveat is that the changes you make to the objects won't be reflected in the background MOC so you'll have to reconcile them. What I did was make the background MOC a private queue MOC and the foreground one is a child of it like this:
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateManagedObjectContext setPersistentStoreCoordinator:coordinator];
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_privateManagedObjectContext];
}
Now when I make changes in the main thread, I can reconcile them easily by doing this:
if ([self.managedObjectContext hasChanges]) {
[self.managedObjectContext performBlockAndWait:^{
NSError *error = nil;
ZAssert([self.managedObjectContext save:&error], #"Error saving MOC: %#\n%#",
[error localizedDescription], [error userInfo]);
}];
}
I wait for it's return since I'm going to reload the table data at this point, but you can choose not to wait if you'd like. The process is pretty quick even for 30K+ records since usually only one or two are changed.
Hope this helps those who are stuck with this!

Deleting entity in MagicalRecord is not persisting

I am having a strange issue with MagicalRecord. Deletes will not persist. When I delete, NSFetchedResultsControllerDelegate correctly sees that the object has been deleted. However, if I close and reopen the app, the entity reappears.
The code I am using to delete the entity is:
ActivityType *activityType = [_fetchedResultsController objectAtIndexPath:indexPath];
[activityType deleteInContext:[NSManagedObjectContext MR_defaultContext]];
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
The code I am using for setting up the NSFetchedResultsController is:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"ActivityType" inManagedObjectContext:[NSManagedObjectContext defaultContext]];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[NSManagedObjectContext defaultContext] sectionNameKeyPath:nil
cacheName:#"activityTypes"];
_fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
Based on other SO posts, I also tried to use [NSManagedObjectContext rootSavingContext] in both the setup and deletion (but to no avail).
I've been through hell and back with core data, and I learned a few things. I'm tired so I'll just write a quick summary.
When you delete an entity, core data may reject it due to your deletion rules. The reason why my deletes didn't go through is because it needed to be cascade but it was nullify. I think it has to do with somehow leaving entities abandoned. I don't know why that would be cause to prevent deletion, but that's what fixed it in my case. The way I discovered it was through log, I saw some statement about a referenced dependent entity, and I realized that delete rules will apply.
When the log says something about a serious error and a listener, check the FRC code. Since this is the listener, your culprit code will be here somewhere. In my case, I disabled [tableview beginUpdates] and [tableview endupdates]. The FRC actually needs this (I thought it was optional). Otherwise, you get some error about inconsistency and managedobjectcontextlistener and how rows need to be added or deleted etc. etc.
when you delete, it may actually get saved into the memory local context, but may not get saved to the persistent store. this means that the FRC delegate code will see the changes, but it may not get saved. also, the memory store may not do the deletion rules checks as it passed mine. but the persistent store will do the checks. gotta look into this more.

How to debug NSFetchRequest

I am using Core Data on iOS and am having problems with NSFetchRequest.
If I create an NSFetchRequest like so:
NSFetchRequest* request = [ NSFetchRequest fetchRequestWithEntityName:#"Recipe"];
and then create instances of my Recipe entity, they appear in the results of the fetch request as expected. But once I save the database, and try to reload them then my fetch request returns no results.
The database saves successfully, with no errors, and using SQLite database browser I can see that all my entities have been saved in the underlying SQLite database. But they won't load...
is there an easy way to diagnose what is happening under the hood in a NSFetchRequest so I can find out why it won't load my entities?
// load recipes unsorted
NSFetchRequest* request = [ NSFetchRequest fetchRequestWithEntityName:#"Recipe"];
request.sortDescriptors = [[NSArray alloc] init];
NSFetchedResultsController* frc = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.recipeDatabase.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
// fetch results empty
NSError* error = nil;
[frc performFetch:&error];
Sorry.... I found my problem. I was not opening the UIManagedDocument when the state was UIDocumentStateClosed. Strange that everything else seems to work when operating on the documents managedObjectContext, even though the document is closed.

Resources