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.
Related
I've got a more or less basic Master Detail app. I've followed this tutorial to get familiar with Core Data, and now I'm trying reorder my cells on my Master TVC. Everything is working fine, including the successful reordering of my cells. However, when I dig down and view one of the detail VCs, I return to the original, alphabetized ordering. I believe it has something to do with the NSSortDescriptor "sortDescriptor" that was included in the tutorial. I am not sure how to remove it, or how to give it different characteristics. Any help is appreciated. Below is my NSFetchedResultsController method.
-(NSFetchedResultsController*) fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *context = [self managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Top" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"topName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
fetchRequest.sortDescriptors = sortDescriptors;
_fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
EDIT:
After much research over the past few days, I'm realizing it's more of an issue with my moveRowAtIndexPath method. Anyone have any suggestions or recommendations of working with Core Data and the ability to reorder cells? Also, does this require custom tableviewcell class?
Your table view is sorted based on a name. Given that the names are fixed, if you reorder cells 'by hand' and then return to the table view, the sorting based on the name is reestablished.
To get what you want you'll need to add a field to your Core Data model called something like sortIndex. You then sort the table with a sortDescriptor based on sortIndex; you can initialize sortIndex to the creation order. When the User reorders cells through the UI, you'll also change the sortIndex for all the impacted cells/managed-objects. Then, since sortIndex is part of the Core Data model, when the User ends and restarts your App, their preferred sorting will be reestablished.
Sometimes, for modeling reasons, you don't want to include a sortIndex directly in some managed object. For example, a Person has first and last names but not a sortIndex (in fact). You can create an association, perhaps a DisplayPreferences managed object, with a one-to-one mapping between a Person and a DisplayPreferences and having a sortIndex in DisplayPreferences.
A few things you need to check here:
You need to actually perform the fetch the first time by calling performFetch: on the NSFetchedResultsController. Either do this in the getter method or it should be done in viewDidLoad.
Have you implemented the required NSFetchedResultsControllerDelegate methods properly? That includes controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:, controller:didChangeSection:atIndex:forChangeType: and controllerDidChangeContent:. The code is all boilerplate but you need to make sure it is there.
What do your UITableViewDatasource methods look like? Make sure the sections and row counts look like this:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[[self fetchedResultsController] sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
return [sectionInfo numberOfObjects];
}
Make sure you are using the indexPath to grab your object in the tableView:cellForRowAtIndexPath: method. It should look something like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"myCellIdentifier"];
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = rowPackage.title;
return cell;
}
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
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];
Need some advice into: Can NSFetchedResultController do this?
UITableView:
[section name] <= {entity: Section, value: title}
[cell title] <= {entity: Cell, value: title}
Model:
[entity: Section, properties: title] <->> [entity: Cell, properties: title, imgPath]
Trouble:
Count of sections, and their titles is working, can't fetch object from relationship to Cell
Thanks for help...
That should be possible. In fact I think that you can use the "standard" table view data source methods and fetched results controller delegate methods if you create the FRC with the sectionNameKeyPath set to "section.title":
// Fetch "Cell" entities:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Cell"];
// First sort descriptor for grouping the cells into sections, sorted by section title:
NSSortDescriptor *sort1 = [NSSortDescriptor sortDescriptorWithKey:#"section.title" ascending:YES];
// Second sort descriptor for sorting the cells within each section:
NSSortDescriptor *sort2 = [NSSortDescriptor sortDescriptorWithKey:#"title" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObjects:sort1, sort2, nil];
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:#"section.title"
cacheName:nil];
(I have assumed that you have an inverse relationship section from the Cell entity to the Section entity.)
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"];