iOS: Sorting and grouping NSFetchedResultsController - ios

I have two tables with the following structure
MainCategory:
name position hasSubCategories
SubCategory:
name position display belongsToMainCategory
Now I want to display all subcategories (where display attribute = YES) grouped by main category. The main category sections should be sorted as defined by position and the subcategories itself (within the section) by their position attribute. (name by the way can be the same for a certain main category...my tables have more attributes but they aren't relevant to understand the problem).
But my order is completely messed up. Why? Here's my code for the FetchedResultsController:
NSError *error = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"SubCategory"];
NSSortDescriptor *mainCatPosition = [[NSSortDescriptor alloc]
initWithKey:#"belongsToMainCategory.position" ascending:YES];
NSSortDescriptor *subCatPosition = [[NSSortDescriptor alloc]
initWithKey:#"position" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObjects:mainCatPosition,subCatPosition,nil];
request.predicate = [NSPredicate predicateWithFormat:#"display = %#", [NSNumber numberWithBool:YES]];
[self.db.managedObjectContext executeFetchRequest:request error:&error];
self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
managedObjectContext:self.budgetDatabase.managedObjectContext
sectionNameKeyPath:#"belongsToMainCategory.name"
cacheName:nil];

The key path used as sectionNameKeyPath: argument to the fetched results controller must
be the same key that is used in the first sort descriptor or generate the same relative ordering.
The fetched results controller first orders all fetched objects according to the first
sort descriptor and then groups the objects into sections according to the sectionNameKeyPath. Therefore using different key paths (as in your case "belongsToMainCategory.position" vs. "belongsToMainCategory.name") does not work.
This could even cause a runtime error about "out of order sections".

Related

NSFetchedResultsController - grouping results from a single entity

Let's say that my Core Data model contains the following entity, Family:
I'd like to use NSFetchedResultsController to display the contents of the Family in a UITableViewController in the following format where parents are "sections" with children listed under a "parent":
In my view controller file, I have the following:
- (void)setFRC
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:[Family entityName]];
NSSortDescriptor *sort1 = [[NSSortDescriptor alloc] initWithKey:#"Child" ascending:YES];
NSSortDescriptor *sort2 = [[NSSortDescriptor alloc] initWithKey:#"Name" ascending:YES];
NSArray *sorters = [NSArray arrayWithObjects:sort1, sort2, nil];
[request setSortDescriptors:sorters];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:[MYCoreDataManager managedObjectContext] sectionNameKeyPath:#"Child" cacheName:nil];
}
When using the code above, however, I'm getting the following table instead:
Can somebody tell me how to get the "children" grouped underneath the proper "parent"? I realize that the data model could be separated such that there are separate entities for child and parent; however, I'm working with legacy code and don't have the luxury of modifying the data model for the time being.
I think you need to use predicates:
Get all Parents
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"Child == %#", #NO];
Loop through the parents and get all their Children
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"Child == %# AND Parent == %#", #YES, parent.Name];
This solution results in an array construct. Not sure, this answer is helping you, since you're looking for a NSFetchedResultsController.

NSFetchRequest / NSFetchedResultsController returning single property distinct but sorted using another attribute of the entity

I am using an NSFetchedResultsController to display data in a table. The data store contains lots of rows that have a groupID, and multiple entities can share a groupID.
Each entity has an insertion date property as well.
I would like to get a list of distinct groupID ordered by the insertion date property. The following code works, except it is not sorted by the insertion date property. I assume that since that property is not one of the ones being fetched, it is not available for sorting.
And I am using MagicalRecord as well, fwiw.
Is there a way to use other properties of the entity for sorting but not have them as part of the result? Adding in the insertion date to the fetched properties makes distinct superfluous since the dates are all unique as they are.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"userID like[c] %#", THEUSERUSERID];
NSFetchRequest *fetchRequest = [AnEntity MR_requestAllWithPredicate:predicate];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setResultType:NSDictionaryResultType];
[fetchRequest setPropertiesToFetch:#[#"groupID"]];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"inDate" ascending:NO];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
_frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[NSManagedObjectContext MR_rootSavingContext] sectionNameKeyPath:nil cacheName:nil];
Chadbag ... I had the same problem. I researched the setPropertiesToFetch and found out it requires an NSArray of NSPropertyDescriptions ... not just the attribute name.
I think if you REMOVE this:
[fetchRequest setPropertiesToFetch:#[#"groupID"]];
and ADD this:
NSDictionary *entityProperties = [entity propertiesByName];
NSPropertyDescription *propDescription = [entityProperties objectForKey:#"groupID"];
NSArray *propArray = [NSArray arrayWithObject:propDescription];
[fetchRequest setPropertiesToFetch:propArray];
That should work for you ... it did for me!

Core Data and NSSortDescriptor not sorting according to third descriptor based on NSString

I have an NSFetchRequest that is fetching an entity called "Player" and I want the results be sorted by 3 attributes in the following order:
Whether the attribute called "sendingoffOffence" is nil or not
Whether the relationship in the entity at "team.homeMatch" is nil or not
I want the strings in the "number" attribute to be sorted in ascending order.
Basically, I am looking to have all players that have a red card ("sendingOffOffence" is not nil) appear at the top of the list, then have the set be ordered by whether the player is on the home team or not, and then finally get the set of players on a team in an offense group be sorted by their jersey numbers.
As such, I use the following code in my fetch request:
// Set the entity for the fetch request
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Player"
inManagedObjectContext:self.match.managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"match == %#", self.match];
[fetchRequest setPredicate:predicate];
// Sort the results
// Sort according to whether the user has a red card / sendingOffOffence
NSSortDescriptor *hasRedCard = [[NSSortDescriptor alloc] initWithKey:#"sendingoffOffence" ascending:NO];
// Sort according to whether the user is on the home team or not
NSSortDescriptor *isHomeTeam = [[NSSortDescriptor alloc] initWithKey:#"team.homeMatch" ascending:NO];
// Sort according to the player's jersey numbers
NSSortDescriptor *number = [[NSSortDescriptor alloc] initWithKey:#"number" ascending:YES selector:#selector(localizedStandardCompare:)];
[fetchRequest setSortDescriptors:#[hasRedCard, isHomeTeam, number]];
// Set the amount of records to retrieve from the database
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:self.match.managedObjectContext
sectionNameKeyPath:nil
cacheName:#"MatchCache"]
However, when I execute the above code, I get the following results:
This sorting order is wrong because I want the following order to appear in the red card section:
11 - Home
12 - Home
0 - Away
11 - Away
21 - Away
And in the yellow card section:
1 - Home
2 - Home
99 - Home
1 - Away
31 - Away
It looks like the yellow card section sorts correctly but the red card section is showing very weird behavior that makes it appear that it is not getting sorted at all.
I am fairly stumped as to why the red card section fails to sort correctly - any thoughts? Should I just sort these objects in memory instead of relying on core data to get my preferred order?
Please be aware that this is a core data application with a SQL backed persistence store.
UPDATE
The following is the SQL statement that core data is using in my fetch:
CoreData: annotation: fetch using NSSQLiteStatement <0x11c77cd0> on entity 'SPlayer' with
sql text 'SELECT t0.Z_ENT, t0.Z_PK FROM ZFSMANAGEDOBJECT t0 LEFT OUTER JOIN ZSTEAM t1 ON
t0.ZTEAM = t1.Z_PK WHERE ( t0.ZMATCH1 = ? AND t0.Z_ENT = ?) ORDER BY
t0.ZSENDINGOFFOFFENCE DESC, t1.ZHOMEMATCH DESC, t0.ZNUMBER COLLATE NSCollateFinderlike '
This is the line that is not being properly applied (for the RED card case):
NSSortDescriptor *isHomeTeam = [[NSSortDescriptor alloc] initWithKey:#"team.homeMatch" ascending:NO];
Without seeing your data model and some of the sample data, it's hard to say why this would not be sorting properly. I would assume this would put NIL values first, then proper values next (so sorting ascending NO would do what it's doing in the YELLOW card case).
If I was in this situation, I would check my assumptions about sort order and these team.homeMatch properties. Do a test where this is the only sort description.
Then focus in on what is different with the RED/YELLOW card condition.
We done without NSFetchedResultsController like this check it....
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Player"
inManagedObjectContext:self.match.managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"match == %#", self.match];
[fetchRequest setPredicate:predicate];
// Sort the results
// Sort according to whether the user has a red card / sendingOffOffence
NSSortDescriptor *hasRedCard = [[NSSortDescriptor alloc] initWithKey:#"sendingoffOffence" ascending:NO];
// Sort according to whether the user is on the home team or not
NSSortDescriptor *isHomeTeam = [[NSSortDescriptor alloc] initWithKey:#"team.homeMatch" ascending:NO];
[fetchRequest setSortDescriptors:#[hasRedCard, isHomeTeam]];
// Set the amount of records to retrieve from the database
[fetchRequest setFetchBatchSize:20];
NSArray *resultArray = [self.match.managedObjectContext executeFetchRequest:request error:&error];
NSSortDescriptor *number = [[NSSortDescriptor alloc] initWithKey:#"number" ascending:YES selector:#selector(localizedStandardCompare:)];
NSArray *sortedArray = [resultArray sortedArrayUsingDescriptors:[NSArray arrayWithObjects:number, nil]];

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.

how to fetch a specific B managed object from a to-many relationship from A to B, based on one of B's attributes?

That is, I have an entity A with a to-many relationship with entity B. B instances have, among other attributes, a creationDate. And I also already have a managed object for A in memory.
So I know I can access all B instances through A's accessor. But I'd like to do something along the lines of:
"for all Bs in A.relationshipName, return the managed object for the B with the most recent B.creationData"
How could I go about it?
How about performing another fetch request:
Assuming the relationship name on the one of the one-to-many side is a
// 1
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"B"
inManagedObjectContext:self.managedObjectContext];
fetchRequest.entity = entity;
// 2
fetchRequest.fetchLimit = 1;
// 3
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"self.a = %#", previouslyFetchedAObject];
// 4
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"creationDate"
ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
fetchRequest.sortDescriptors = sortDescriptors;
In plain English
Make a new fetch request for B
Set the fetch limit so you only return 1 object
Set a predicate that only pulls back objects that have a relationship to the already fetched A object
Set the order descending on date so you get newest dates first
Alternately, since you already have a managed object A in memory, you could do an in-memory filtered sort of the A->>B relationship.
A *inMemoryA = ...
id byDate = [NSSortDescriptor sortDescriptorWithKey:#"creationDate" ascending:YES];
id sorters = [NSArray arrayWithObjects:byDate, nil];
NSArray *sortedBs = [inMemoryA allBs] sortedArrayUsingDescriptors:sorters];
B *mostRecentB = [sortedBs lastObject];

Resources