koen
koen

Reputation: 5729

Optimizing UITableView backed by NSFetcchedResultsController

I recently switched my CoreData backed UITableViews 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

Answers (1)

Florian Mielke
Florian Mielke

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


Update

Have you tried to set a fetchBatchSize in your NSFetchedResultsController's fetchRequest? Docs: NSFetchRequest fetchBatchSize


Update 2

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

Related Questions