Optimizing UITableView backed by NSFetcchedResultsController - ios

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;
}

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 == %#.#min.order", 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.

Related

How to give a sorting predicate in Core data model editor [duplicate]

The Core Data Documentation states that:
The fetch request associated with the [fetched] property can have a sort ordering, and thus the fetched property may be ordered.
How do I specify the sort descriptors for the fetched property in Xcode's data model editor? I can't find a relevant field anywhere. I'm developing for the iPhone platform, if this makes any difference.
If this is not possible via the graphical model editor, how do I go about modifying the fetch request for the fetched property in code so that it has a sort descriptor?
You can actually grab the model fetched property and add the sort descriptors to it (again, in code). I did this in the standard method that XCode generates in your AppDelegate if you choose one of the templates with Core Data:
By the way. This sorts ALL fetched properties on ALL models in your data model. You could get fancy and adaptive with it, but it was the most succinct way to handle sorting the 7 separate models that each had fetched properties that needed to be sorted by name. Works well.
/**
Returns the managed object model for the application.
If the model doesn't already exist, it is created by merging all of the models found in the application bundle.
*/
- (NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel != nil) {
return managedObjectModel;
}
managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
// Find the fetched properties, and make them sorted...
for (NSEntityDescription *entity in [managedObjectModel entities]) {
for (NSPropertyDescription *property in [entity properties]) {
if ([property isKindOfClass:[NSFetchedPropertyDescription class]]) {
NSFetchedPropertyDescription *fetchedProperty = (NSFetchedPropertyDescription *)property;
NSFetchRequest *fetchRequest = [fetchedProperty fetchRequest];
// Only sort by name if the destination entity actually has a "name" field
if ([[[[fetchRequest entity] propertiesByName] allKeys] containsObject:#"name"]) {
NSSortDescriptor *sortByName = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortByName]];
[sortByName release];
}
}
}
}
return managedObjectModel;
}
You don't specify them in the graphical editor (as far as I know).
You specify them in the code where you make the fetch.
NSFetchRequest* request = [[NSFetchRequest alloc] init];
NSEntityDescription* entity = [NSEntityDescription entityForName:#"whatYouAreLookingFor"
inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];
// here's where you specify the sort
NSSortDescriptor* sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:#"name" ascending:YES];
NSArray* sortDescriptors = [[[NSArray alloc] initWithObjects: sortDescriptor, nil] autorelease];
[request setSortDescriptors:sortDescriptors];
[sortDescriptor release];
fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:#"myCache"];
The modeling tool doesn't appear to have a way to set the sort descriptors on the fetch request.
It should be possible[1] to, after loading the model but before associating it with a persistent store coordinator, to find the fetched property descriptions for which you want to control the sort order, and replace their fetch requests with fetch requests that have sort descriptors set on them.
[1] In principle this should work. In practice, I have not done so or tested it.
Using Tim Shadel's great answer I added per-NSManagedObject subclass sorting...
...in Tier.m (which is a NSManagedObject subclass)...
+ (void)initialize
{
if(self == [Tier class])
{
NSFetchedPropertyDescription *displayLessonPropertyDescription = [[[Tier entityDescription] propertiesByName] objectForKey:#"displayLesson"];
NSFetchRequest *fetchRequest = [displayLessonPropertyDescription fetchRequest];
NSSortDescriptor *sortByName = [[NSSortDescriptor alloc] initWithKey:#"displayOrder" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortByName]];
[sortByName release];
}
}
For a single fetched property, Swift 4, Xcode 9.4:
// retrieve the fetched property's fetch request
let fetchedPropertyRequest = (modelName.entitiesByName["entityName"]!.propertiesByName["fetchedPropertyName"] as! NSFetchedPropertyDescription).fetchRequest
// set up the sort descriptors
let sortDescriptors = [NSSortDescriptor(key: "keyName", ascending: true)]
// add the sort descriptors to the fetch request
fetchedPropertyRequest!.sortDescriptors = sortDescriptors
Here's the same thing the loooonnnnnnggggggg way:
// retrieve the fetched property's fetch request
let theEntityDescription: NSEntityDescription = modelName.entitiesByName["entityName"]!
let theFetchedPropertyDescription = theEntityDescription.propertiesByName["fetchedPropertyName"]! as! NSFetchedPropertyDescription
let theFetchedPropertyRequest = theFetchedPropertyDescription.fetchRequest
// set up the sort descriptors
let sortDescriptor1 = NSSortDescriptor(key: "keyName", ascending: true)
let theSortDescriptors = [sortDescriptor1]
// add the sort descriptors to the fetch request
theFetchedPropertyRequest!.sortDescriptors = theSortDescriptors
Note: for this example, I force-unwrapped values. Make sure that you account for optional values in your actual code!
Sadly, though, the ability to sort is somewhat limited. For example, you cannot take a field that is an NSString containing a number, and sort it numerically, at least not with a SQLite backing store. As long as you are sorting alphabetically on strings, numerically only on values stored as numbers and so forth, though, the NSSortDescriptor applied to the fetch request works just fine.
Put this into your NSManagedObject subclass:
+ (void)initialize
{
if (self != [EntityManagedObjectSubClass class]) return;
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];
NSEntityDescription *entityDescription = [managedObjectModel entitiesByName][#"entityName"];
NSFetchedPropertyDescription *fetchedPropertyDescription = [entityDescription propertiesByName][#"fetchedPropertyName"];
NSFetchRequest *fetchRequest = [fetchedPropertyDescription fetchRequest];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"sortDescriptorKey" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
}
Replace EntityManagedObjectSubClass, entityName, fetchedPropertyName and sortDescriptorKey with your own stuff.
Jeff, if the strings are right-aligned, you could just sort on the strings; " 123" > " 23" and so on. But iirc ascii space is after the numbers, and if so, then what you would do is create a dynamic property that is an NSNumber (which supports the compare: method), and use the numberFromString: method to make a number from the string. Then you can specify the number field in the sort. In the interface:
#property NSString *stringIsaNumber; // in the data model
#property NSNumber *number;
in the implementation:
#dynamic stringIsaNumber;
- (NSNumber *) number ;
{ return [self.stringIsaNumber numberFromString]; }
- (void) setNumber:(NSNumber *)value;
{ self.stringIsaNumber = [NSString stringWithFormat:#"%5i",value) }
ps plz forgive coding errors, this is off the top of my head.

How to implement a transient property on core data?

I have this core data entity called Countries. This entity has a field called nameOfCountry that contains country names in english. I need to localize this to other languages, so I have created a transient property called nameOfCountryLocalized.
On the Countries class I am importing this category
Countries+NameOfCountryLocalized.h
#import "Countries.h"
#interface Countries (NameOfCountryLocalized)
#property (nonatomic, strong) NSString * nameOfCountryLocalized;
#end
Countries+NameOfCountryLocalized.m
#import "Countries+NameOfCountryLocalized.h"
#import "Countries.h"
#implementation Countries (NameOfCountryLocalized)
#dynamic nameOfCountryLocalized;
-(NSString *) nameOfCountryLocalized {
[self willAccessValueForKey:#"nameOfCountryLocalized"];
NSString *nameLocalized = NSLocalizedString(self.nomePais, nil);
[self didAccessValueForKey:#"nameOfCountryLocalized"];
return nomeLocalizado;
}
-(void)setNameOfCountryLocalized:(NSString *) nameLocalized {
[self willChangeValueForKey:#"nameOfCountryLocalized"];
[self setNomePaisLocalizado:];
[self didChangeValueForKey:#"nameOfCountryLocalized"];
}
#end
when I try to access nameOfCountryLocalized using this from a tableViewController
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Countries" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"nameOfCountryLocalized" ascending:YES];
[fetchRequest setSortDescriptors:#[sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:#"Root"];
_fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
I see this error:keypath nameOfCountryLocalized not found in entity
any clues?
The NSFetchedResultsController cannot sort by a transient property. The FRC applies the sort to the underlying SQL store. With a transient property the FRC can group results of a fetch into sections. This group is created by assigned the transient property to sectionNameKeyPath.
Pulling all entities into an array will become a pain once your data grows. The FRC does provide nice support for larger data sets and you don't really want to loose that.
You might be ok assuming that your users don't switch language very often. If that is the case I suggest you write the localized country name into the store as a "normal" property and the FRC can do the sort on that property.
Although when a user does switch language then you would need to update all Country entities.
Your Countries class is not an entity. It is a class for an individual object in the database.
You really should rename it to Country, as it's incorrect to refer to NSManagedObject subclasses as plural. There's a reason Apple didn't name the class NSManagedObjects.
Because your property is added to individual objects but not the entities themselves, it is only available after objects have been fetched from the database. It cannot be used as part of fetching the objects.
You're going to need to fetch the results first (all of them) into an NSArray and then sort the objects by applying the NSSortDescriptor to the NSArray. You might want to do this by creating a wrapper class around NSFetchedResultsController.
Alternatively, put a nameOfCountryLocalized property in the actual database, with english values, and then if the user doesn't use english write to the database changing everything to the correct values. This would allow you to use NSFetchedResultsController exactly as you're trying to do now. I recommend this approach if your database is huge... but it's not, there are only a couple hundred countries in the world so performance is a total non-issue.

Fetch Data According To Specific Entity

I am using magical record to fetch data from core data, using the code:
- (NSFetchedResultsController *)fetchedResultsController {
if (!_fetchedResultsController ) {
_fetchedResultsController = [TagItem MR_fetchAllGroupedBy:nil withPredicate:nil sortedBy:#"name" ascending:YES delegate:self];
}
return _fetchedResultsController;
}
The problem is, it is populating a table view controller that is pushed to by another table view controller, so basically a user selects an option which takes the person to a list of data, but the data within the option is specific to that option. So what I want to do is fetch only the data that is assigned to that option, rather than simply displaying all the data available. My method above only allows me to obtain all the data, rather than selecting specific data according to the option. The data within the option is given the objectId of the option itself as an entity and thus provides a relationship between the two.
So my question is, how do I go about fetching data according to a specific entity, using magical record?
UPDATE
I have tried setting the predicate to a specific value just to test if it works, thinking it would work the same as just working with core data alone, but I just get an error.
- (NSFetchedResultsController *)fetchedResultsController {
if (!_fetchedResultsController ) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name = test"];
_fetchedResultsController = [TagItem MR_fetchAllGroupedBy:nil withPredicate:predicate sortedBy:#"name" ascending:YES delegate:self];
}
return _fetchedResultsController;
}
The error is as below, even though I know for certain there is a data item with the name test stored, nonetheless it doesn't seem to work.
'NSInvalidArgumentException', reason: 'Unable to generate SQL for predicate (name == test) (problem on RHS)'
That's what the withPredicate parameter is for:
NSPredicate *predicate = ... // predicate that filters the items to display
_fetchedResultsController = [TagItem MR_fetchAllGroupedBy:nil withPredicate:predicate ...];
For example, if TagItem has a to-one relationship person to Person, then
Person *person = ...; // person selected in the first view controller
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"person = %#", person];
_fetchedResultsController = [TagItem MR_fetchAllGroupedBy:nil withPredicate:predicate ...];
would fetch all items related to that person.

Filtering a UITableView based on an NSPredicate from an NSFetchedResultsController

I'm trying to filter objects in a UITableView, which gets its data from a Core Data sqlite via aNSFetchedResultsController. It's working fine before filtering, but now I need to be able to tap a button and have it display only objects one of whose properties (a BOOL) is YES. Lets call it isFavourite.
I'm a little confused as to where to begin. I understand the concept of a predicate, and I've constructed this one, which should work:
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"isFavourite == 1"];
However, I'm a little confused as to how to attach that predicate to the NSFetchedResultsController. I already allocated and initialised it, using a separate fetch request which simply returned all objects of that entity (and that works perfectly). If it's been initialised already, can I change its fetch request? If so, how do I do that, then how do I make it re-run the query. Do I then need to manually update the UITableView with reloadData or similar. Then, what do I do when I want to switch this filter off and go back to viewing all results?
I've read a lot of existing questions here but most seem to relate to search bars, which make the answers a lot more complicated. I've also read the Apple documentation, but tend to find its written in a pretty cryptic fashion. My code is below, I'm only part-way through the filterFavourite method and that's where I'm looking for help and clarification.
- (NSFetchedResultsController *) fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName: #"Quote" inManagedObjectContext: [[CoreDataController sharedCoreDataController] managedObjectContext]];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey: #"quoteID" ascending: YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize: 50];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest: fetchRequest managedObjectContext: [[FQCoreDataController sharedCoreDataController] managedObjectContext] sectionNameKeyPath: nil cacheName: nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
- (IBAction) filterFavourite: (id) sender
{
if (isDisplayingFavourite) {
// Switch to showing all..
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"isFavourite == 1"];
isDisplayingFavourite = NO;
} else {
// Switch to showing favourites only..
isDisplayingFavourite = YES;
}
}
EDIT: The delegate method "controllerDidChangeContent" is never called in this code. I can't figure out why that is, either. Hugely, hugely confusing, Core Data is like a brick wall. In fact, NONE of the four delegate methods are ever called.
You don't attach a predicate to the NSFetchedResultsController-- you attach it to the NSFetchRequest. It has a method called setPredicate: that does exactly what its name suggests.
You can modify the fetch request by updating the fetchRequest attribute and calling performFetch: again. You could also nil out your fetchedResultsController ivar, and make the code above flexible enough to add whatever predicate(s) would be useful. The next time your fetchedResultsController method is called, it will create a new instance for you with the current predicate(s). That consolidates all of your NSFetchedResultsController code in one method. It's probably slightly more expensive in CPU usage but also probably easier to maintain.

Core Data sort tableview by formatted date

I know how to sort Core Data objects in a tableview by NsDate, but this by default seems to create a new section for each object. I want to sort them by a medium formatted date with NSDateFormatter. How would I do this?
For example, if I have 3 objects created on the same day, I want them to be in the same section with the section title being that Day, no time needed.
Each object has an NSDate property. Thanks for your help.
This is the code I have in fetchedResultsController with rgeorge's suggestions. What am I missing here?
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
NSLog(#"get old fetched controller");
return fetchedResultsController;
}
else{
NSLog(#"get new fetched controller");
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"InTextEntity" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *dateDescriptor = [[NSSortDescriptor alloc] initWithKey:#"dateModified" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:dateDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:#"mediumFormattedDate" cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return fetchedResultsController;
}
(I'll write this up assuming you're using an NSFetchedResultsController to drive your tableview. If you're not, I recommend checking it out.)
An interesting feature of NSFetchedResultsController's sectioning abilities: although the property you sort on must be a modeled property (because sqlite does the actual sorting), the property you group the sections with need not be. The only requirement is that the grouping be consistent with the ordering. (i.e., sorting by the sort property will put the objects with matching group properties next to each other.)
So just add something like this to your modeled object class:
// in interface
#property (nonatomic, readonly) NSString *mediumFormattedDate;
// in impl
-(NSString *)mediumFormattedDate
{
// this can be fancier if you need a custom format or particular timezone:
return [NSDateFormatter localizedStringFromDate:self.date
dateStyle:NSDateFormatterMediumStyle
timeStyle:NSDateFormatterNoStyle];
}
(no need to mention mediumFormattedDate in the .xcdatamodel at all.)
Then go ahead and sort your objects by the date property, but group them by your new property. When you create your NSFetchedResultsController, do so along these lines:
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:#"MyFancyEntity"];
NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey:#"date"
ascending:YES];
[fr setSortDescriptors:[NSArray arrayWithObject:sd]];
NSFetchedResultsController *frc =
[[NSFetchedResultsController alloc] initWithFetchRequest:fr
managedObjectContext:myManagedObjectContext
sectionNameKeyPath:#"mediumFormattedDate"
cacheName:nil];
// then do stuff with frc
That's all it takes! I've done this in a few apps to get date grouping and it works well.
Sounds like you're setting the section index on the fetched results controller to be your date property, which seems undesirable.
Instead you should probably be computing the section index yourself, and sorting by date. You can accomplish this in either your data model or by computing the sections manually in code.
For example, you could add a property to your managed object model called "Day" and set that to whatever value you want to use (you don't specify if its something like Monday or an actual date like 21).
You can then pass that property to the fetched results controller.
Alternatively you could implement the sections yourself, days are easy, its Monday-Sunday. Dates are a bit harder, 1-28,30,31 depending on what month it is. Then use an appropriate NSPredicate / NSFetchRequest to get the count of the items in each section.

Resources