Reputation: 5729
I recently switched my CoreData backed UITableView
s to use a NSFetchedResultsController
instead of an NSArray
. For one of the tables, scrolling is now very slow, and I think I know why, but I don't know yet what would be the best solution to fix this.
There are two entities Book
and Author
which are in a many-to-many relationship. Each cell displays the book title, plus the author. If there is more than one author, it will just display the main author. Each Author
has an "order" attribute, which is set when the data is imported.
What I have been doing so far is every time the author name is accessed, my Book
class returns a mainAuthor property (an NSString
):
- (NSString *) mainAuthor
{
if (!mainAuthor)
{
NSSortDescriptor *sortOrder= [NSSortDescriptor sortDescriptorWithKey: @"order" ascending: YES];
NSArray *authorsSorted = [self.authors sortedArrayUsingDescriptors: @[sortOrder]];
Author *a = authorsSorted[0];
mainAuthor = [NSString stringWithFormat: @"%@", a.name];
}
return mainAuthor;
}
For whatever reason this is now called many times instead of only once and causing the slow down. Maybe NSFetchedResultsController
fetches the references over and over when scrolling the table?
So how can I fix this? One possibility is to make mainAuthor
an attribute instead of a property. So it is set immediately when the data is imported. But before I start messing with my dataModel I'd like to know if this would be the way to move forward, or maybe there is an alternative solution?
UPDATE 1: Here is the code where I set up the fetchController:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSManagedObjectContext *moc = [NSManagedObjectContext MR_defaultContext];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName: @"Book"];
// sort the books by publishing date
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey: @"date" ascending: YES];
[fetchRequest setSortDescriptors: @[sort]];
// only get the books that belong to the library of the current viewController
NSPredicate *predicate = [NSPredicate predicateWithFormat: @"libraries contains[cd] %@", self.library];
[fetchRequest setPredicate: predicate];
[fetchRequest setRelationshipKeyPathsForPrefetching: @[@"authors"]];
[fetchRequest setFetchBatchSize: 10];
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest: fetchRequest
managedObjectContext: moc
sectionNameKeyPath: nil
cacheName: nil];
frc.delegate = self;
_fetchedResultsController = frc;
return _fetchedResultsController;
}
Upvotes: 0
Views: 132
Reputation: 3360
Based on your sample code I'd assume that mainAuthor
is not in your Core Data schema. As Core Data handles the lifetime (faulting) of your managed objects you should add this property to your Book
entity to avoid unpredictable results by using a transient attribute.
Despite this I'd recommend to return an Author
object instead of a NSString
as you might change name of the attribute in the future or want to use additional information of the mainAuthor
in your UI.
Transient Attribute
Add a transient attribute mainAuthor
to your Book
's entity and add a custom accessor to your Book
's class:
- (Author *)mainAuthor
{
[self willAccessValueForKey:@"mainAuthor"];
Author *value = [self primitiveValueForKey:@"mainAuthor"];
[self didAccessValueForKey:@"mainAuthor"];
if (value == nil)
{
NSPredicate *filterByMinOrder = [NSPredicate predicateWithFormat:@"order == %@[email protected]", self.authors];
value = [[self.authors filteredSetUsingPredicate:filterByMinOrder] anyObject];
[self setPrimitiveValue:value forKey:@"mainAuthor"];
}
return value;
}
The disadvantage of using a transient is that you have to make sure that the data is always up-to-date during the lifetime of the appropriate book
. So you have to reset mainAuthor
in:
willTurnIntoFault
awakeFromFetch
awakeFromSnapshotEvents:
(Optional, but necessary if the user can change the data)
addAuthorObject:
removeAuthorObject:
by calling [self setPrimitiveValue:nil forKey:@"mainAuthor"]
.
Hint: Better and faster is to create a synthesized primitiveMainAuthor
instead of using primitiveValue:forKey:
: Managed Object Accessor Methods
Have you tried to set a fetchBatchSize
in your NSFetchedResultsController
's fetchRequest
? Docs: NSFetchRequest fetchBatchSize
Yes, setting the appropriate relationship in setRelationshipKeyPathsForPrefetching
is necessary in that case.
To identify bottlenecks it's also really helpful to set the debug argument -com.apple.CoreData.SQLDebug 1
to see the SQL statements created by Core Data. This also often helps to understand the different NSFetchRequest
attributes and their impacts.
Upvotes: 1