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;
}
Related
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.
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.
I have a strange bug: if I uncomment my NSPredicate, the resulting UITableView is empty.
My data Model is the following:
Category <-->> Feed <-->> Post
I am fetching the Posts. Post.feed is a Post's Feed. Feed has an rss NString property.
Here's the code:
- (NSFetchedResultsController *)fetchedResultsController {
// Set up the fetched results controller if needed.
if (_fetchedResultsController == nil) {
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Post"
inManagedObjectContext:_globalMOC];
[fetchRequest setEntity:entity];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"feed.rss == %#", _detailItem.rss];
[fetchRequest setPredicate:predicate];
// 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:_globalMOC
sectionNameKeyPath:nil
cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
self.fetchedResultsController.delegate = self;
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. If it is not possible to recover from the error, display an alert
// panel that instructs the user to quit the application by pressing the Home button.
//
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
return _fetchedResultsController;
}
I only see results if I uncomment the NSPredicate. I tried with LIKE, ==, =, with double and single quotes around %#...
The best would be to directly compare the Feed object...
Anyone can help me?
Solved here.
The problem was that the NSFetchedResultsController was initialized once before the _detailItem was even set.
I have a UITableView With a custom Cell that contains another UItableView. I need to pass an object to it, and then execute a fetchrequest based on this object.
From my cellForRowAtIndexPath method, I am passing a Managed Object to the custom Cell, like this:
TWHistoryViewStandardExpendedCell *cell = (TWHistoryViewStandardExpendedCell *)[tableView dequeueReusableCellWithIdentifier:ExpandedCell];
if (cell == nil) {
cell = (TWHistoryViewStandardExpendedCell *)[[[NSBundle mainBundle] loadNibNamed:#"HistoryViewStandardCellExpanded" owner:self options:nil] objectAtIndex:0];
}
Day *aDay = (Day *)[_fetchedResultsController objectAtIndexPath:indexPath];
[cell setViewingDay: aDay]; // NSLog here returns the expected object! :)
return cell;
This way, I should be able to execute a fetchRequest based on this object, from my custom Cell.
On my custom UItableViewCell, I do this:
- (void) awakeFromNib
{
[self fetchRequest];
}
- (void)fetchRequest {
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
Some details about my fetchedResultsController: see in comments that my managed object returns null here!
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController) {
return _fetchedResultsController;
}
NSManagedObjectContext * managedObjectContext =
[myAppDelegate managedObjectContext];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"Clock"
inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:#"day == %#", _viewingDay]];
NSLog(#"_viewingDay: %#", _viewingDay); // returns null! :(
NSLog(#"_viewingDay.clocks: %#", _viewingDay.clocks); // also returns null!
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"clockIn" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
//
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
_fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Based on this, I have overriden my setViewingDay method:
- (void)setViewingDay:(Day *)viewingDay
{
if (_viewingDay != viewingDay) {
_viewingDay = viewingDay;
NSLog(#"setViewingDay: %#", _viewingDay); // returns expected object! =)
[self fetchRequest];
}
}
Still, after this, my UITableView remains empty. My numberOfRowsInSection method keeps returning 0!
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
NSLog(#"numberOfRowsInSection %d", [sectionInfo numberOfObjects]);
return [sectionInfo numberOfObjects];
}
I also added a UIButton that performs the fetch request. Hoping to find a situation where the fetchRequest was being performed before the actual setting of my reference to the managed object. So I click on it, do perform a fetch. And nothing! No rows whatsoever.
EDIT:
However, the NSLogs bellow, from inside my fetchResultsController do return an object then, by using a supporting UIBUtton and IBAction. But still no rows!
NSLog(#"_viewingDay: %#", _viewingDay);
NSLog(#"_viewingDay.clocks: %#", _viewingDay.clocks);
Supporting UIButton to perform a fetch:
- (IBAction)fetchem:(UIButton *)sender {
[_clocksTableView reloadData];
_fetchedResultsController = nil;
[NSFetchedResultsController deleteCacheWithName:nil];
[self fetchRequest];
NSLog(#"\n\nfetchem: %# \n\nvd: %# \n\nclocks: %# ", _fetchedResultsController.description, _viewingDay.description, _viewingDay.clocks.description );
}
Anything I might be missing here?
Thank you!
EDIT 2:
I just realized that setting my predicate to (1 == 1), returns all clocks. Confirming that my resultsController is correctly set up. There may be something wrong with my predicate... I don't see what. I have a similar predicate in a previous controller and works great.
Predicate is very simple, nothing fancy:
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:#"day == %#", _viewingDay]];
I have a Clock and Day entities.
A Day has many clocks. 1 to many relationship.
A clock has one day, and one day only.
The fetchRequest above should have been returning all clocks from that day. But it's not?
1) awakeFromNib gets execute the moment you instantiate the cell and therefore before you set the viewing day. It also doesn't get execute if the cell is reused, so you should trigger the fetch request separately.
2) How does the fetchedResultsController property inside the cell get set?
EDIT 1:
From the datamodel and the predicate, it would seem the relationship between clock and day doesn't get set correctly.
EDIT 2:
Quoting the asker's comment from below: "The problem was fixed after inserting: [self fetchRequest] inside my overriden setter."
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.