NSFetchedResultsController with item in multiple sections? - ios

I have a managed object to represent a spot on a map. These points have zero-many types. I want to also show a sectioned table view. I had this working with an NSFetchedResultsController when type was a single value on the MapPOI object. But now that the types are in a different objects with a relationship between "MapPOI" called "types", how do I write the query (can I?)
Original:
- (NSFetchedResultsController *)newFetchedResultsControllerWithSearch:(NSString *)searchString
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MapPOI"];
if(searchString.length)
{
request.predicate = [NSPredicate predicateWithFormat:#"name CONTAINS[cd] %#", searchString];
}
request.sortDescriptors = [NSArray arrayWithObjects:[NSSortDescriptor sortDescriptorWithKey:#"type" ascending:YES ],[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES ],nil ];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.campus.managedObjectContext
sectionNameKeyPath:#"type"
cacheName:nil];
aFetchedResultsController.delegate = self;
NSError *error = nil;
if (![aFetchedResultsController performFetch:&error])
{
NSLog(#"Error performing institution fetch with search string %#: %#, %#", searchString, error, [error userInfo]);
}
return aFetchedResultsController;
}
i tried something like
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.campus.managedObjectContext
sectionNameKeyPath:#"types.name"
cacheName:nil];
But that caused
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid to many relationship in setPropertiesToFetch: (types.name)'

The NSFetchedResultsController is very flexible and can be used with all kinds of views, not only table views.
The sectionNameKeyPath is of course wrong. It clearly has to be a to-one relationship. Assuming that one MapPOI can only have one type, the key path should be something like #"type.name".
If, however one MapPOI can have several types, you could do the following:
Fetch the types entity rather than the POI entity. You do not need a section key path. Now objectAtIndexPath:indexPath.row will fetch a Type managed object.
For number of sections use
self.fetchedResultsController.fetchedObjects.count
For section titles use
[[self.fetchedResultsController.fetchedObjects objectAtIndex:section] name];
For row count in section use
Type *type = [self.fetchedResultsController.fetchedObjects objectAtIndex:section];
type.mapPOIs.count;
And it should be obvious how to populate the cell with the MapPOI entities.

Related

Core Data - sectionNameKeyPath with a One to Many Relationship

I'm having difficulty creating tableView sections using a relationship.
I have two entities with a relationship List <----->> Item.
I want the List to be the sections and the Item to be the rows. I set the sectionNameKeyPath with a key path #"itemList".
And here's what the rest of my fetchedResultsController looks like
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
// Fetch Request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Item"];
[fetchRequest setFetchBatchSize:20];
// Sort Descriptors
NSSortDescriptor *itemSort = [[NSSortDescriptor alloc] initWithKey:#"displayOrderItem" ascending:YES];
NSSortDescriptor *sectionSort = [[NSSortDescriptor alloc] initWithKey:#"displayOrderList" ascending:YES];
NSArray *sortDescriptors = #[sectionSort, itemSort];
[fetchRequest setSortDescriptors:sortDescriptors];
// Fetched Results Controller
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"itemList" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _fetchedResultsController;
}
The result is that the fetchedResultsController doesn't populate the tableView at all. When I try it without sections, with sectionNameKeyPath:nil and just setSortDescriptor:itemSort, it populates the tableView fine. Also, numberOfSectionsInTableView and controller didChangeSection is properly set up.
I'm not sure what I'm doing wrong. Can anybody help me with this?
Thanks
Change the section name key path to itemList.listName as the FRC is expecting a string name for the section, not a managed object 'representing' that section.

NSFetchedResultsController not combining like sections

I'm trying to use NSFetchedResultsController to display data in a table view. My data model has an array of users, each user has an array of categories, each category has an array of organizations, and each organization has an array of accounts. The table view displays data from a single user. Each row represents an organization belonging to the user, and the organizations are separated into sections, with each section containing the organizations belonging to a specific category. In my app delegate I populate my application with some dummy data, and NSFetchedResultsController displays that data fine. The problem is that when I try to add a new organization to an existing category the row is added but it is added to a new section containing only that row, as opposed to merging with the section that holds the rest of the organizations for that category.
Here is the code for my NSFetchedResults controller:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"PKOrganization" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Tell the fetch request to only retrieve the organizations from the proper user
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:#"(category.user.name = '%#')", self.user.name]]];
// Sort the data
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"category.name" ascending:YES];
NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:#"name" 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:#"category" cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
It might also be worth noting that I am able to tell that the data is added as expected. It just doesn't get displayed correctly.
Try changing this line
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"category" cacheName:#"Master"];
To this:
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"category.name" cacheName:#"Master"];
In my experience, I needed the sectionNameKeyPath to be exactly the same as the first sortDescriptor in order to get sections to work properly.

Exception on Core Data save

This seems a fairly common workflow I have an issue with that I cannot figure why. I have a UITableView with NSFetchedResultsController supplying the rows. To add a new object, I insert into context and then present a new view controller to edit the details. The add button action is like so:
-(IBAction)addNewIssueType:(id)sender
{
IssueType *newUntitledType = [NSEntityDescription insertNewObjectForEntityForName:#"IssueType" inManagedObjectContext:self.managedObjectContext];
newUntitledType.name = NSLocalizedString(#"Untitled Task", #"Name for newly created task");
[self saveContext];
self.editingType = newUntitledType;
[self presentNameEditViewForType:newUntitledType];
}
I am crashing with exception at the saveContext method, error:
2013-05-11 16:25:35.990 App [18843:c07] CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. The left hand side for an ALL or ANY operator must be either an NSArray or an NSSet. with userInfo (null)
2013-05-11 16:25:35.992 App [18843:c07] * Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'The left hand side for an ALL or ANY operator must be either an NSArray or an NSSet.'
* First throw call stack:
Taking the notification observer suggestion as a clue, I looked at my NSFetchedResultsController, presumably the only observer of the NSManagedObjectContext in scope:
-(NSFetchedResultsController *)typesController
{
if (_typesController) {
return _typesController;
}
// Set up the fetched results controller.
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"IssueType" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Filter out the required types
NSPredicate *exclude = [NSPredicate predicateWithFormat:#"NONE name in %#",excludedTypes];
[fetchRequest setPredicate:exclude];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[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:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.typesController = aFetchedResultsController;
NSError *error = nil;
if (![_typesController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
[self failAlertWithMessage:nil forceAbort:YES];
}
return _typesController;
}
I found that, if I comment out the lines creating and assigning the predicate, it does not crash. Of course then I am showing some objects that I want hidden. I can't see anything wrong with the predicate itself, fetch does return the expected objects and excludes those in the excludedTypes Array as intended.
Save context method does not have any issues on editing or deleting existing objects, only on inserting a new object.
I am not using and threads here other than the main thread.
NONE is an alias for NOT ANY and is indeed for to-many relationships.
What you probably want is:
[NSPredicate predicateWithFormat:#"NOT name in %#", excludedTypes];

FetchedResultsController with sections

I am writing a small app in which I uses coredata, I have data like subjects which contains
Maths, Science, and other Books.
Other books can be added or deleted, but maths and science cannot be deleted,they will get added by default when new student is added. When I fetch my results, i should get all the book names including maths and science.
What I want do is display the data in three section with headers as Maths, Science, and Others. Maths and science will contain only one row, i.e, maths or science. And all other books should be in reading section.
How to proceed to achieve this?
When you create your NSFetchResultsController use the entity name for the books table in the fetch request.
Then use this...
NSFetchedResultsController *aController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"typePropertyName" cacheName:nil];
typePropertyName will be the path to get from a book to the name of the section it will be in.
It could just be #"typeName" if you have it directly in the Book table or it could be #"type.name" if you have a relationship to a table called type and then that table has a field called name.
Anyway, that will create a NSFetchedResultsController with the sections in...
Full code will be something like ...
#pragma mark - fetched results controller
- (NSFetchedResultsController*)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Book"];
[request setFetchBatchSize:20];
NSSortDescriptor *sdType = [[NSSortDescriptor alloc] initWithKey:#"type.name" ascending:YES];
NSSortDescriptor *sdName = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[request setSortDescriptors:#[sdType, sdName]];
NSFetchedResultsController *aController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"type.name" cacheName:nil];
aController.delegate = self;
self.fetchedResultsController = aController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
Then in the tableViewController you can have this...
- (NSString*)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
return [sectionInfo name]
}
This will then use the section name as the header for each section.

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