FetchedResultsController with sections - ios

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.

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.

Core Data to-many relationship fetching

I have the following set up in core data: A Dish entity, with many ingredients, and a Ingredient entity with one dish.
I'm having trouble fetching my to-many relationship data, i.e. the ingredients for a certain dish. I'm trying to populate my collection view cells with the ingredients associated to each dish.
I have my core data model set up as follows:
I have tried various methods to fetch the data.
One of the things I've tried is to do the following: Dish *dish = [self.fetchedResultsController objectAtIndexPath:indexPath]; and then NSLog dish.ingredients (which returns a NSSet), but the returns the whole core data item, i.e., the createdAt date, the name and everything, together with the unformatted version of the ingredients for that dish.
Another thing i tried was to create a new instance of the Ingredient entity with Ingredient *ingredient = [dish.ingredients valueForKeyPath:#"ingredientName"];, but that returns an unformatted string (I want it UTF8 encoded before it's displayed because it contains special characters).
A lot of solutions when googling suggests using NSPredicate, but I just can't figure out how to use that. I'm still new to core data, and XCODE in general, so any help here would be appreciated, i.e. where do you implement the NSPredicate, and what would the correct predicate be in my case?
A bit of supporting code:
my collectionViewController:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSManagedObjectContext* context = [RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext;
self.managedObjectContext = context;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Dish" 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:#"createdAt" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[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:#"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;
}
cellForItemAtIndexPath:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
FOFPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"photo" forIndexPath:indexPath];
self.object = [self.fetchedResultsController objectAtIndexPath:indexPath];
Dish *dish = [self.fetchedResultsController objectAtIndexPath:indexPath];
Ingredient *ingredient = ???;
cell.backgroundColor = [UIColor lightGrayColor];
[cell.imageView setImageWithURL:[NSURL URLWithString:[[NSString stringWithFormat:#"http://192.168.0.3:4000%#", dish.dishImage] description]] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
}];
cell.titleLabel.text = dish.dishName;
cell.headerLabel.text = dish.dishHeader;
cell.ingredientsLabel.text = [NSString stringWithFormat:#"%#", nil];
NSLog(#"%#", ingredient);
[self.collectionView reloadItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
return cell;
}
The simple way is your first approach and then only use the attributes that you care about from the ingredients. Core Data is more about programming with objects than it is about the underlying database, so attempting to simulate DB queries isn't usually the right approach.
I also recommend getting rid of all the valueForKey stuff and using actual object properties so that the code is a bit clearer and the compiler can help you out more with data types.
If you're looking for Ingredients only for a one specific Dish, you have to set your fetchRequest for Ingredients (and not Dish) and set the predicate on Dish. To make the following code work, you may need first to Create NSManagedObject Subclasses (with the help of Xcode Editor button).
#import "Dish.h"
#import "Ingredient.h"
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Ingredient"];
[fetchRequest setFetchBatchSize:20];
//if dishname are unique values, set your predicate on it.
//You can also set your predicate on the dishID if dishName are not unique values (several dishes with a same name)
NSString *theDishIWant = #"Ratatouille"; //whatever Dish name you want
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"dish.dishName == %#", theDishIWant];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"ingredientname" ascending:YES];
[fetchRequest setSortDescriptors:#[sortDescriptor];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
NSLog (#"%#", _fetchedResultsController);
return _fetchedResultsController;
}
Your configureCell: atIndexPath: method may look like this:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Ingredient *ingredient = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = ingredient.ingredientName;
cell.detailTextLabel.text = ingredient.ingredientValue;
}

FetchedResultsController and displaying data from 3 nested entities in one UITableViewCell

Could somebody explain me how can I display in one tableViewCell data from three entities connected by relationship?
I have 3 entities, let it be User, Device, and Alert. Relationships : User has many Devices and these devices have many Alerts.
In my AlertsTableViewController I want to display every alerts with user_name property which is included in User entity.
Should I start my fetch request by setting entity for name User or Alert?.
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"????????" inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:12];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"created_at" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:_managedObjectContext sectionNameKeyPath:nil cacheName:#"Alerts"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _fetchedResultsController;
}
My TableView methods:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"AlertsTableViewCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
User *u = [[self fetchedResultsController] objectAtIndexPath:indexPath];
//I know how to display User with details of devices by looping nsset but how to display every alerts with User.name?
for (Device *device in u.devices) {
[cell.textLabel setText:device.someProperty];
}
return cell;
}
When you like to display Alerts, in most simple cases (like yours) you will set the fetch request entity to #"Alert".
To select only Alerts that belong to a given user you will need to define a predicate for your fetch request:
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"device.user = %#",<User object or User objectID>];
The User object or objectID will be known to you from the app user selection, or, if you have another unique identifier for each user (like the userName property) you could include that in your predicate:
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"device.user.name = %#",userNameVariable];
In most cases, the LAST thing you want to do is loop over a set of NSManagedObjects as they will fire faults one by one and give you a really bad user experience (especially in cellForRow...).
Get the data you like to display before you need to supply it to the view.
Edit:
In that spirit, if you like to display the someProperty value in your AlertsTableViewController you should also add:
fetchRequest.relationshipKeyPathsForPrefetching = #[#"device"];
So that the Device will be fetched along with Alert object.
Obviously, you have to change your cellForRow... method to accept Alert objects from the FRC.

IOS Core Data Cardinality issue

I'm writing a simple application using TableViews and Core Data that shows a list of students in the first level and when clicked on a cell, it shows the names of the courses that student takes. I set student->courses as toMany, courses->student as toOne. This is how i pass the Student entity to course view controller for using its managedObjectContext:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
Student *info = [_fetchedResultsController objectAtIndexPath:indexPath];
CoursesTableViewController *courseViewController = [[CoursesTableViewController alloc] initWithStudentInfo:info];
[self.navigationController pushViewController:courseViewController animated:YES];
}
This is how i create courses inside the courses view controller(I want only three courses as named below, numberofrow is supposed to assure that):
- (void)viewDidLoad
{
[super viewDidLoad];
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][0];
int numberofrow = [sectionInfo numberOfObjects];
NSLog(#"%d",numberofrow);
NSLog(#"%d",[self.fetchedResultsController sections].count);
if(numberofrow <=2){
NSLog(#"creating courses");
[self insertNewObject:#"comp319"];
[self insertNewObject:#"comp314"];
[self insertNewObject:#"comp316"];
}
}
- (void)insertNewObject:(NSString *) str{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:str forKey:#"course_name"];
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
How i create the fetchedResultsController inside courses View Controller:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Courses" inManagedObjectContext:self.studentInfo.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:#"course_name" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[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.studentInfo.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
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;
}
Finally the problem is that when I create the first student and click on it, NSLog in viewDidLoad of courses view controller above prints "creating courses", however when i create the second student it does not and also numberofrow prints 3 where it should have printed 0 since it is a new student instance.
Thanks for any help.
Replace
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Courses" inManagedObjectContext:self.studentInfo.managedObjectContext];
[fetchRequest setEntity:entity];
With
// my comment : entity here has to be your student name, why 'course'
// and also replace your search key from 'yourCourseNameKey' with 'yourStudentNameKey'
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Your Student Name" inManagedObjectContext:self.studentInfo.managedObjectContext];
[fetchRequest setEntity:entity];
You need to get number of Students but you query number of Courses
You do not set a predicate on the fetch provided to the fetched request controller so you aren't searching for courses for the selected student and creating then if they aren't there. Not that that is ideal anyway (and wouldn't work because you aren't setting a relationship between the student and all of the courses).
So, most likely you should have a set of courses and a set of students. The relationship should be many:many as a course really has multiple students and each student takes multiple courses (or could anyway).
Now, your students and courses would be created 'up-front' and then your view controllers simply deal with fetching, displaying and connecting (establishing the relationships) the objects.

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.

Resources