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.
Related
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).
I'm trying to execute a fetch against a fairly large data set (~60000 rows) in the background of my app. Despite using a separate thread, the UI noticeably hangs for a second or so whenever the fetch executes. Is my approach correct?
- (id)init
{
if(self = [super init])
{
ABAppDelegate *appDelegate = (ABAppDelegate *)[[UIApplication sharedApplication] delegate];
_rootManagedObjectContext = appDelegate.managedObjectContext;
_backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundContext setPersistentStoreCoordinator:_rootManagedObjectContext.persistentStoreCoordinator];
}
return self;
}
- (void)fetch {
[_backgroundContext performBlock:^{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"ItemPhoto"];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"full_uploaded_to_server == 0 OR thumb_uploaded_to_server == 0"];
fetchRequest.predicate = pred;
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"modified" ascending:YES]; //Start at the back of the queue
fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
fetchRequest.fetchBatchSize = 1;
fetchRequest.fetchLimit = 1;
NSError *error;
NSArray *photos = [_backgroundContext executeFetchRequest:fetchRequest error:&error];
if(error != nil) {
NSLog(#"Fetch error: %#", error.localizedDescription);
}
}];
}
Looking in Instruments, it's definitely the call to executeFetchRequest: that's taking a long time to complete, and it does seem to be running on its own thread. Why would this be causing the UI to hang?
Thanks!
Any activity against the NSPersistentStoreCoordinator is going to cause a block of other activities against it. If you are fetching on one thread and another thread attempts to access the NSPersistentStoreCoordinator it is going to be blocked.
This can be solved in one of two ways:
Reduce the fetches so that the block is not noticeable. Fetching in chunks, for example, can help to reduce this issue.
Make sure the data the UI is exposing is fully loaded into memory. This will stop the main thread from trying to hit the NSPersistentStoreCoordinator and getting blocked.
Depending on the application and the situation, one or the other (or both) of these implementations will remove the issue.
At the end of the day, background threads are not a silver bullet to solve fetch times. If you are planning on fetching on a background thread just to put them onto the main thread you are going to be disappointed with the performance.
If you are fetching on the background thread for a non-UI purpose then consider reducing the size of each fetch or change the batch size, etc. to decrease the fetch time itself.
Update
Warming up the cache doesn't work on iOS like it used to on OS X. You can get the objects fully loaded into memory by configuring your NSFetchRequest so that the objects are fully loaded, relationships are loaded (if needed), your batch size and fetch size are large enough. Naturally this needs to be balanced against memory usage.
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!
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.
Good Morning.
I have a problem when I run my app on my device, it lags/stutters when I scroll in the main tableView.
I've narrowed the problem down to a call to core data from inside my tableCell
--In cell for row at indexPath
person is a custom class and contact manager is my file with all my calls to core data and manipulating data
person.contactSelected = [contactManager checkContactSelectedStatus:person];
--In my contactManager file the call goes to this function.
and just updates the contacts selected status (when the user presses a button to change from being allowed in the call or not in the call)
-(NSNumber *) checkContactSelectedStatus:(ContactPerson *)person{
SWNAppDelegate *delegate = [[UIApplication sharedApplication]delegate];
NSManagedObjectContext *context = [delegate managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Contact" inManagedObjectContext:context];
NSFetchRequest *req =[[NSFetchRequest alloc]init];
[req setEntity:entity];
NSPredicate *pred =[NSPredicate predicateWithFormat:#"(recordID like %#)",[person.recordID stringValue]];
[req setPredicate:pred];
NSError *error;
NSManagedObject *checkStatus = [[context executeFetchRequest:req error:&error] objectAtIndex:0];
person.contactSelected = [checkStatus valueForKey:#"isSelected"];
return person.contactSelected;}
Is there an easy way to throw this into a Queue? I have read and tried to figure out how to send a NSManagedObject to queues, but when I create a child of the Parent MoC, It can not find the Entity "Contact". I don't know if there is a simpler way to do it or not!?
Thanks for your time, and
WhatWasIThinking!?!?!
Yes, this is really inefficient code. The fetch has to be done multiple times, i.e. for each cell as it becomes visible.
You should instead use an NSFetchedResultsController which is especially designed to work with table views. It will decide the appropriate number of trips to the store for fetches and optimize for speed and memory footprint.
Also, you will most likely use significantly less code.
Besides, a predicate string like recordID like %# does not make much sense. If you are using IDs they should be unique so you can index them and look them up really fast. LIKE, just like string functions such as CONTAINS are very slow in comparison.