Custom Core Data SectionNameKeyPath - ios

I am new at core data and am trying to figure out how to create a custom sectionNameKeyPath in my NSFetchedResultsController. I have a managed object with an attribute called acctPeriod. It is a NSString. I want to create sections based on the first 4 characters of this field. The first 4 characters represent the year of the accounting period and doesn't need to be saved.
I have gone through this site and have seen posts about transient attributes but I can't seem to get them to work. Basically I want this and then assign periodYear for my sectionNameKeyPath.
#dynamic periodYear;
-(NSString *)periodYear
{
return [self.acctPeriod substringToIndex:4];
}
Any help would be appreciated.
**UPDATE:
Using Martin R. answer, I was able to get it to work as expected.
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Billing" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"acctPeriod" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
//Predicate
NSPredicate *pred = [NSPredicate predicateWithFormat:#"clients = %#", self.client];
NSLog(#"%#",pred);
//[fetchRequest setResultType:NSDictionaryResultType];
//[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setPredicate:pred];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"periodYear" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}

The following should work: Implement the periodYear method (which will be used
as "section name key path") in a class extension of your managed object subclass:
#interface Event (AdditionalMethods)
- (NSString *)periodYear;
#end
#implementation Event (AdditionalMethods)
- (NSString *)periodYear {
return [self.acctPeriod substringToIndex:4];
}
#end
Make sure that acctPeriod is used as the first (or only) sort descriptor for the fetch request:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"acctPeriod" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
Use periodYear as sectionNameKeyPath for the fetched results controller:
NSFetchedResultsController *_fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"periodYear"
cacheName:nil];
_fetchedResultsController.delegate = self;
self.fetchedResultsController = _fetchedResultsController;
And finally add the default titleForHeaderInSection method:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
Alternatively, you can define periodYear as transient attribute of the managed object.
It will also not be stored in the database in that case, but can be implemented in a way that the value is calculated on demand and cached.
The DateSectionTitles sample project from the Apple Developer Library demonstrates how this works.

I recommend against using a transient property as the sectionNameKeyPath as it will result in faulting all objects obtained by the fetch request just so the property could be used as the section path.
You better define a persistent property of year and use it as your sectionNameKeyPath.
set you FRC sectionNameKeyPath to year like so:
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"year"
cacheName:nil/*your chioce*/];
to display the section name as a title in the table, implement:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id<NSFetchedResultsSectionInfo> sec = [self.fetchedResultsController sections][section];
return [sec name];
}

Related

Core Data family members grouped by surname

This is my first attempt on Core Data so I would like your guidance.
My example project is quite simple, I would like to create an iOS app that displays a list of people. What I am looking for is to group all members of the same family together. Some people in the list do not belong to a "family". So, UITableView is going to be a mixture of groups and rows.
Here is my Model so far.
and here I am adding data in the context
Family *newFamily = [NSEntityDescription insertNewObjectForEntityForName:#“Family” inManagedObjectContext:self.managedObjectContext];
[newFamily setSurname:[dict objectForKey:#“surname”]];
NSMutableSet *members = [newTeam mutableSetValueForKey:#"members"];
for (NSDictionary *member in [dict objectForKey:#"members"]) {
Member *newMember = [NSEntityDescription insertNewObjectForEntityForName:#"Member" inManagedObjectContext:self.managedObjectContext];
[newMember setFirstName:[member objectForKey:#"firstName"]];
[newMember setAge:[member objectForKey:#“age”]];
[members addObject:newMember];
}
//Save context ...
In UITableViewController I am able to read and display just families'/groups' name. I am confused on how do you display all the objects of a section.
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Family" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"surname" ascending:NO];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
// 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:#"surname" cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _fetchedResultsController;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// I DON'T KNOW WHAT TO DO HERE ...
// return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
return [sectionInfo name];
}
First, you need to amend your model to include the inverse relationship, from Member to Family, which presumably is "to-one". Name that relationship "family". (This inverse relationship is needed in this case, see below, but you should almost always include inverses for all relationships, regardless of whether you think you need them.)
Next, amend your FRC configuration as follows: the underlying entity should be Member, since each row of your tableView will represent a Member object. The sectionNameKeyPath should be "family.surname" so the FRC will automatically put the Member objects into sections based on the surname attribute of the related Family object. It is important that the sort order for the FRC matches up with the sections (ie all Member objects in the same section must be sorted together), so amend the sort descriptors to use family.surname (you could then sort by firstName if desired):
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Member" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *surnameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"family.surname" ascending:NO];
NSSortDescriptor *firstNameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"firstName" ascending:NO];
[fetchRequest setSortDescriptors:#[surnameSortDescriptor, firstNameSortDescriptor]];
// 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:#"family.surname" cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _fetchedResultsController;
}
Finally, there is boilerplate code for linking an FRC to a tableView (see the Apple Docs here). The piece you are missing is:
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
if ([[self.fetchedResultsController sections] count] > 0) {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
} else
return 0;
}
Your configureCell method will likewise need to determine the correct Member object to display using
Member *currentMember = [self.fetchedResultsController objectAtIndexPath:indexPath];
Note that the FRC will log a warning message if there are Member objects that are not in a Family, since the sectionName will be nil. But it will group these all together - you might want to amend the titleForHeaderInSection to use a different name for that section.
Also, note that the FRC's sections are determined by inspecting the family.surname for each Member object. If there are Family objects that have no members, those Family objects will not appear in the table view (i.e. no empty sections).

Attempting to reordering UITableView using an NSFetchedResultsController

I am attempting to edit the order of my UITableView while using Core Data and an NSFetchedResultsController. As I understand, Core Data does not have a built in method for rearranging objects in a Core Data model.
The idea was to create an array, reorder my objects there, and then write that data back to my model.
NSFetchedResultsController
-(NSFetchedResultsController *) fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *context = [self managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"List" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"listName" 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;
}
moveRowAtIndexPath
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; {
NSMutableArray *toDoItems = [[self.fetchedResultsController fetchedObjects] mutableCopy];
NSManagedObject *managedObject = [[self fetchedResultsController] objectAtIndexPath:sourceIndexPath];
[toDoItems removeObject:managedObject];
[toDoItems insertObject:managedObject atIndex:[destinationIndexPath row]];
int i = 0;
for (NSManagedObject *moc in toDoItems) {
[moc setValue:[NSNumber numberWithInt:i++] forKey:#"listName"];
}
[self.managedObjectContext save:nil];
}
When I hit my Edit button, I can rearrange the rows but as soon as I let the row go, my app crashes. Im not getting any kind of stack trace in the console when it crashes.
I set a breakpoint on exception and it seems to be crashing on this line
[moc setValue:[NSNumber numberWithInt:i++] forKey:#"listName"];
My key name is correct. But I now realize this is completely wrong in that I am trying to set this as a number and that shouldnt be the case.
Any suggestion or push in the right direction would be appreciated.
Either amend your code to set a string value for the listName, something like this:
[moc setValue:[NSString stringWithFormat:"%d",i++] forKey:#"listName"];
(but beware because by default this will sort as a string, so 11 comes before 2, etc).
So, better, add an integer attribute to your model, and use that to sort the fetched results controller.
Don't copy NSManagedObject. It's a managed object by core data. If you want a new one ask the context.

Problems with NSFetchedResultsController in iOS7?

I am using NSFetchedResultsController to load some data into UITableView. No matter how many objects I save, it only loads the first one. If I delete that one object from the UITableView and reload it, sometimes it will show one or more of the other objects I've saved. It's very unpredictable.
The strange thing is the code I was using worked fine on the iOS6 SDK. I know that the issue is with the NSFetchedResultsController because when I make a fetch request with NSFetchRequest, the data that is returned is correct. Here is the code;
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSError *error;
if (![[self fetchedResultsController] performFetch:&error])
{
exit(-1);
}
}
- (void)viewDidUnload
{
self.fetchedResultsController = nil;
}
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"HatInfo" inManagedObjectContext:[self.appDelegate managedObjectContext]];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"lastSaved" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:9];
NSArray *fetchedObjects = [[self.appDelegate managedObjectContext] executeFetchRequest:fetchRequest error:nil];
//returns the correct amount of objects
NSLog(#"FetchedObjects: %lu", (unsigned long)[fetchedObjects count]);
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[self.appDelegate managedObjectContext] sectionNameKeyPath:nil
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
//returns the incorrect amount
return [sectionInfo numberOfObjects];
}
This kind of unexpected behavior can happen if you used the same cacheName in other part of core data code (for example: another fetchedResultsController in your app.
You can try to change the cache name to something different or nil and see what happens.
Add this line before you start fetch :
[NSFetchedResultsController deleteCacheWithName:<< catch name>>];

numberOfRowsInSection: for a specific attribute

I have a UITableView that is populated with data from core data and I currently have it set up so that it adjusts the size of the table based on the number of objects in an entity. My problem is that I know have two attributes in this entity. Now the tableview has a bunch of extra cells because it is creating one for everything in the entity, not just creating one per object in a specific attribute. So I would like to set numberOfRowsInSection: based on amount of objects in an attribute, not the whole entity. How would I do this?
How I am currently doing it:
id sectionInfo =
[[_fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
EDIT:
This is what I have tried:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
// id sectionInfo =
// [[_fetchedResultsController sections] objectAtIndex:section];
// return [sectionInfo numberOfObjects];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *context = [self managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"FeedEntity" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"imageData != nill"];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
id sectionInfo =
[[theFetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
But this results in this error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'
Ok, well thats an easy solve. Will there always be an image for every single string? Why not just return the yourStringDataArray.length in the -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section method? This way you will only get the amount of cells needed to display your total amount of strings in your array.
so you have one entity with two attributes. And you need to filter the objects to show for the tableview. I think you need to use NSFetchRequest and NSPredicate to filter your objects by checking the attribute of your object.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *context = [your context];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"FeedEntity" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"imageData = nil"];
request.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:#"text" ascending:NO]];
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];

Filtering a UITableView from NSFetchedResultsController and Core Data with NSPredicate

I have a UITableView using NSFetchedResultsController to display a list of users.
I added a UISegmentedControl to switch between my full list of users and only my active users.
To get my list of users, I use fetchedResultsController :
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"User" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"lastname" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
if (self.barSegmentedControl.selectedSegmentIndex == 1) {
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"active == YES"]];
}
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
To perform an update of the tableview when clicking on the UISegmentedControl, I use segmentedControlIndexChanged :
-(IBAction) segmentedControlIndexChanged{
self.fetchedResultsController = nil;
[self fetchedResultsController];
[self.tableView reloadData];
}
But I'm not sure I'm doing this right.
Could you say me if this is the right way to filter a UITableView with NSFetchedResultsController ?
I also wanted to know if it is possible to filter a list with an animation ?
Exactly like in the iPhone Phone App when in the Recents Tab, it switch from all calls to missed calls ?
Thanks for your help.
Your approach will work fine. I too using the same thing without any issues.
Coming to animation you can do them in its delegate methods,
Check Apple's Documentation here.
self.fetchedResultsController = nil; is a reasonable way to invalidate a property.
But you should delete this weird [self fetchedResultsController]; line.
[self.tableView reloadData]; will call your delegate which should access fetchedResultsController property (if you're using _fetchedResultsController variable in your datasource methods, that would be an anti-pattern).
filter a list with an animation
Prepare a list of rows to hide and send
[self.tableView deleteRowsAtIndexPaths: ... withRowAnimation:...]
instead of reloadData.

Resources