Ordering core data objects inside a table view section - ios

I am using NSSortDescriptors to order core data objects and create table view sections depending on a date attribute. Using this kind of SortDescriptor, the table view sections are ordered as expected, but the rows inside the sections are also ordered by the same date attribute. Is there a way to have another ordering system inside each section? I guess the main problem is that core data stores date objects with date+time values, that way there are no objects with exactly the same date value. But I would need to order the objects inside the same section based on another attribute.
Thank you.
And here is my code for the NSFetchedResultsController:
-(NSFetchedResultsController*)fetchedResultsController{
if (_fetchedResultsController != nil){
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]init];
NSManagedObjectContext *context = self.managedObjectContext;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"MyEntity" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]initWithKey:#"itemDate" ascending:YES];
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc]initWithKey:#"itemName" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc]initWithObjects:sortDescriptor,sortDescriptor1, nil];
fetchRequest.sortDescriptors = sortDescriptors;
_fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:#"sectionIdentifier" cacheName:nil];
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
EDITED QUESTION
This is the piece of code added to the cellForRowAtIndexPath method:
int indexpathsection = indexPath.section;
[self sortedSectionForIndex:indexpathsection];
NSLog(#"INDEXPATH= %ld", (long)indexPath.section);
And this is the method proposed y Marcus:
- (NSArray*)sortedSectionForIndex:(NSInteger)index
{
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"priority" ascending:YES];
id section = [[self fetchedResultsController] sections][index];
NSLog(#"INDEX********* = %ld", (long)index);
NSArray *objects = [section objects];
NSArray *sorted = [objects sortedArrayUsingDescriptors:#[sort]];
return sorted;
}

The NSFetchedResultsController is meant for the sort order to be controlled via a single sort or set of sorts. It is not intended to do what you are trying to do.
But you can do it, it is just not as clean.
First, the discussion of sorting a section. This does not require you to write your own sorting algorithm:
- (NSArray*)sortedSectionForIndex:(NSInteger)index
{
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"MyKey" ascending:YES];
id section = [[self fetchedResultsController] sections][index];
NSArray *objects = [section objects];
NSArray *sorted = [objects sortedArrayUsingDescriptors:#[sort]];
return sorted
}
With that method, you can ask for a section to be sorted whenever you want to retrieve an object. However that is inefficient as you are sorting every time you want to touch an object which in a UITableViewController is a LOT.
Instead, take this example and integrate it with your NSFetchedResultsController and its delegate methods so that you are storing the sorted arrays and keeping them in sync. That way you are only doing the sort when the data changes instead of on each method call.
Update
The sort code I provided to you does not sort what is inside of the NSFetchedResultsController. It takes what is in the section, sorts it and returns you the sorted array. So you need to use what it returns:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSArray *sorted = [self sortedSectionForIndex:[indexPath section]];
id object = [sorted objectAtIndex:[indexPath row]];
id cell = ...; //Now get your cell reference
//Now update your cell with the data from object
return cell;
}

You can normalize the time component of your attribute in the awakeFromFetchmethod of your NSManagedObject.
- (void) awakeFromFetch {
[super awakeFromFetch];
NSDateComponents* comps = [[NSCalendar currentCalendar] components:NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit fromDate:self.itemDate];
self.itemDate = [[NSCalendar currentCalendar] dateFromComponents:comps];
}
In addition, looking at your code, you have to tell the NSFetchedResultsController on what it should base the section breaks. You do this by providing a keyPath for the section breaks when you initialize it. For your case, instead of using sectionNameKeyPath:#"sectionIdentifier", use the first sort key:
_fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:#"itemDate" cacheName:nil];

Related

ios core data to AZ indexed tableview

I'm trying to show the values of my core data model to an A-Z indexed table based on the first letter on my attributes (similar to the iOS address book app). The "Favorites" entity of my core data model has 2 attributes: username and status. I want to display only the usernames with status = accepted to the A-Z indexed table.
Here is my code:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Favorites" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSString *status = #"accepted";
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"status == %#",status];
[fetchRequest setPredicate:predicate];
// Create the sort descriptors array.
NSSortDescriptor *usernameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"username" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:usernameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:#"username" cacheName:#"Root"];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
return fetchedResultsController;
}
Now when I'm trying to access the section name I get (null)
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSLog(#"%#",[[[fetchedResultsController sections] objectAtIndex:section] name]);
return [[[fetchedResultsController sections] objectAtIndex:section] name];
}
Also I thing that with that way I will get the the name and not the first char in order to display it as a section title.
You need to access the sectionsInfo object properly:
id <NSFetchedResultsSectionInfo> info =
[fetchedResultsController sections][section];
return [info name];
However, this will give you a heading for each unique name, probably not want you want. Instead, you have to give your entity a transient property e.g. NSString *sectionIdentifier and write a getter for it that returns the first letter of the username attribute.
If want an index from A-Z running down on the right edge of the table view you additionally have to implement:
sectionIndexTitlesForTableView: and
tableView:sectionForSectionIndexTitle:atIndex:.
If you still get null for your titles, maybe they are not set or persisted in your entity? Maybe you got zero results? Maybe your fetchedResultsController is nil? There are a number of flaws in your data model, so this seems quite possible.
Your entity name Favorites is plural. That is not logical, you should name it Favorite as one instance only describes one favourite.
The status is a string which is also very inefficient. Instead, you should use a number and apply some enum scheme.
The username is a property of Favorite. That seems also very messy because presumably, you also have a User entity which has a username attribute. You should use a relationship to model this.
use NSFetchedResultsController's sectionIndexTitles function to get array of first char

iOS CoreData NSFetchedResultsController sections and sorting

I currently have a CoreData entity that has a name and date attributes, and I would like to create a NSFetchedResultsController that returns the results sectioned by name sorted by date descending (both the sections and its contents), and if possible, only one entry per section. I prefer not to use NSDictionaryResultType.
Lets say I have the following entries:
Name | Date (year/month/day)
-----+----------------------
Anne | 2014/01/16
John | 2014/01/17
John | 2014/01/15
Nick | 2014/01/13
Nick | 2014/01/10
For the above data I wish to obtain only the following results:
Section | Entry Date
--------+-----------
John | 2014/01/17
Anne | 2014/01/16
Nick | 2014/01/13
How do I create the NSFetchedResultsControllerto obtain only the data and in the order listed above?
As of now I have the following code:
NSManagedObjectContext *moc = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"MyEntity" inManagedObjectContext:moc];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"my predicate" argumentArray:...]];
[fetchRequest setPredicate:predicate];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:moc
sectionNameKeyPath:#"name"
cacheName:nil];
The above code sections the data by name, but each section doesn't have its entries sorted by dateAND it has entries that are not from that name!
The result that I want is the table showing unique name sorted by date like showed in the beginning of the post.
Update: I've been unable to do this, so I switched to result type dictionary and used group by for now, I will leave the question open in the hopes someone knows how to do it.
There is no way (that I am aware of) to limit each section to one result in the NSFetchedResultsController.
But, you might not have to do this. You could probably get away with not displaying everything that is not in the first section.
Also, you should be aware that you should not section the request by using any other key than the key used in the primary sort descriptor
If you are using this with a tableView you could try something like this:
FRC creation
NSSortDescriptor *sdName = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSSortDescriptor *sdDate = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
// sort by name, then sort by date
[fetchRequest setSortDescriptors:#[sdName, sdDate]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:moc
sectionNameKeyPath:#"name"
cacheName:nil];
// fetch...
and the tableViewDataSource:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSFetchedResultsController *fetchedResultsController = [self fetchedResultsController];
return [fetchedResultsController.sections count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1; // only display one result for each section
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
NSFetchedResultsController *fetchedResultsController = [self fetchedResultsController];
YourObject *object = [fetchedResultsController objectAtIndexPath:indexPath];
... configure cell ...
Depending on how many objects you have per person this might not be feasible. If there are a lot of entries you won't display I would recommend to normalize the core data model. E.g.:
You then would display a list of Persons. But maybe this normalization would be a good idea regardless of the number of objects per name. Fetching more objects than are displayed smells fishy to me.
Here is a snippet of some code, in regards to my comment
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"EntityThingO"];
NSSortDescriptor *sortDescriptorDate = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
NSSortDescriptor *sortDescriptorName = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
fetchRequest.sortDescriptors = #[sortDescriptorDate , sortDescriptorName];
[fetchRequest setFetchBatchSize:10];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:context
sectionNameKeyPath:#"date"
cacheName:nil];

CoreData NSFetchedResultsController Sorting

I have a small problem with sorting my NSFetchedResultsController.
My NSManagedObject has two attributes. date and startTime.
The date is of time 00:00:00 on all my objects, this way when using date as the sectionNameKeyPath it grabs all the objects with identical dates (by day) into one section. If the time of the dates were different, it would put every object into a different section.
This works well, but then inside each group I want to sort the objects by startTime. So they are list from earliest on that date to the latest in each section respectively.
My issue is when using date as the sectionNameKeyPath and startTime as an NSSortDescriptor` it doesn't like it and plays weirdly. Such as only sometimes showing certain data in what seems like an irregular way.
I think it comes down to having to have the sort descriptor and sectionNameKeyPath the same. Am I right in thinking this? If not, how should I setup my NSFetchedResultsController to list my data in the fashion mentioned?
Thanks.
EDIT: Here is come code... Also worth noting when using startTime as my second sort descriptor, it causes duplicates to display in my tableview with nil objects.
NSFetchedResultsController:
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:#"startTime" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor1, sortDescriptor2];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"date" cacheName:#"Master"];
cellForRowAtIndexPath just a snippet showing how I designate each managed object:
id <NSFetchedResultsSectionInfo> sectionInfo = [self.flightFetchedResultsController.sections objectAtIndex:indexPath.section];
NSArray *sectionFlights = [sectionInfo objects];
Flight *flight = [sectionFlights objectAtIndex:indexPath.row];
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.flightFetchedResultsController.sections.count;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.flightFetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
Your section key name path needs to match the first sort descriptor.
So you could do...
// sectionKeyNamePath = #"date".
NSSortDescriptor *dateSD = [NSSortDescriptor sortDescriptorWithKey:#"date" ascending:YES];
NSSortDescriptor *startTimeSD = [NSSortDescriptor sortDescriptorWithKey:#"startTime" ascending:YES];
frc.request.sortDescriptors = #[dateSD, startTimeSD];
If you do this then it will sort (and section) by date and then sort each section by startTime.
From your code
You are getting the fetched objects incorrectly.
To get an object you need to use...
Flight *flight = [self.frc objectAtIndexPath:indexPath];
A fetched results controller knows about its sections and rows. You don't need to split it apart.

NSFetchedResultsController - different sort order than section name

I have an NSManagedObject for the sections in the grouped UITableView.
This object has the attributes "name" and "createdAt".
I want to use "name" in te UI for the section titles, but sorted by "createdAt".
According to the documentation the first sortDescriptor key has to be also the sectionNameKeyPath of the NSFetchedResultsController.
I suggested using two sortDescriptors, but it doesn't work. The sections are still sorted by name.
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Object" inManagedObjectContext:[CoreDataHelper instance].managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortName = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSSortDescriptor *sortDate = [[NSSortDescriptor alloc] initWithKey:#"createdAt" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:sortName, sortDate, nil]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[CoreDataHelper instance].managedObjectContext sectionNameKeyPath:#"name"
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
return _fetchedResultsController;
}
A fetched results controller (FRC) uses only the first sort descriptor to group (and sort) the objects into sections. A second sort descriptor can be added to sort the objects within each section.
Also, the key path of the sort descriptor must be the same as the sectionNameKeyPath of the FRC (or at least generate the same relative ordering).
See also Creating a Fetched Results Controller in the “Core Data Programming Guide”:
... In this example you add one more NSSortDescriptor instance to the
NSFetchRequest instance. You set the same key from that new sort
descriptor as the sectionNameKeyPath on the initialization of the
NSFetchedResultsController. The fetched results controller uses this
initial sort controller to break apart the data into multiple sections
and therefore requires that the keys match.
In your case, you can proceed as follows:
Use createdAt as sectionNameKeyPath and in the first sort descriptor.
Modify the titleForHeaderInSection delegate function to return the name property instead of createdAt:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.controller sections] objectAtIndex:section];
return [[[sectionInfo objects] objectAtIndex:0] name];
}
Note: If you have multiple objects with the same name but different createAt values, these will be grouped into different sections with the above approach. I don't know if that is a problem for you.
You're almost there. You need to define the sectionNameKeypath in your initWithFetchRequest call.
NSSortDescriptor *sortDate = [[NSSortDescriptor alloc] initWithKey:#"createdAt" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDate]];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[CoreDataHelper instance] sectionNameKeyPath:#"name"];

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