Fetch Data According To Specific Entity - ios

I am using magical record to fetch data from core data, using the code:
- (NSFetchedResultsController *)fetchedResultsController {
if (!_fetchedResultsController ) {
_fetchedResultsController = [TagItem MR_fetchAllGroupedBy:nil withPredicate:nil sortedBy:#"name" ascending:YES delegate:self];
}
return _fetchedResultsController;
}
The problem is, it is populating a table view controller that is pushed to by another table view controller, so basically a user selects an option which takes the person to a list of data, but the data within the option is specific to that option. So what I want to do is fetch only the data that is assigned to that option, rather than simply displaying all the data available. My method above only allows me to obtain all the data, rather than selecting specific data according to the option. The data within the option is given the objectId of the option itself as an entity and thus provides a relationship between the two.
So my question is, how do I go about fetching data according to a specific entity, using magical record?
UPDATE
I have tried setting the predicate to a specific value just to test if it works, thinking it would work the same as just working with core data alone, but I just get an error.
- (NSFetchedResultsController *)fetchedResultsController {
if (!_fetchedResultsController ) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name = test"];
_fetchedResultsController = [TagItem MR_fetchAllGroupedBy:nil withPredicate:predicate sortedBy:#"name" ascending:YES delegate:self];
}
return _fetchedResultsController;
}
The error is as below, even though I know for certain there is a data item with the name test stored, nonetheless it doesn't seem to work.
'NSInvalidArgumentException', reason: 'Unable to generate SQL for predicate (name == test) (problem on RHS)'

That's what the withPredicate parameter is for:
NSPredicate *predicate = ... // predicate that filters the items to display
_fetchedResultsController = [TagItem MR_fetchAllGroupedBy:nil withPredicate:predicate ...];
For example, if TagItem has a to-one relationship person to Person, then
Person *person = ...; // person selected in the first view controller
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"person = %#", person];
_fetchedResultsController = [TagItem MR_fetchAllGroupedBy:nil withPredicate:predicate ...];
would fetch all items related to that person.

Related

Adding a Search Bar to a Table View with Core Data

I have an app that saves data to a Core Data sql then view it in a table view.
The data model:
Entity: Fruits
Attributes: name, picture
So in the table view, cell.textLabel.text = Fruits.name
Each cell has a segue with a view that shows Fruits.picture.
I want to add a search bar to search in Fruits names.
I followed this tutorial: http://www.appcoda.com/search-bar-tutorial-ios7/
But the problem I had is in filterContentForSearchText
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"name contains[c] %#", searchText];
searchResults = [recipes filteredArrayUsingPredicate:resultPredicate];
}
I'm using core data not an array for storing data. And I don't know how to filter it so I can display searchResults in table cells and use it in prepareForSegue.
Since you're using core data and not an array of Fruits you should filter your data this way:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Fruits" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name contains[c] %#", searchText];
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray* searchResults = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
}
To show the picture in a second viewController you should put something like this in you prepareForSegue method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
Fruit *selectedItem;
if (self.searchDisplayController.active) {
selectedItem = [searchResults objectAtIndex:indexPath.row];
} else {
selectedItem = [fruitsList objectAtIndex:indexPath.row];
}
DestinationViewController *destination = segue.destinationViewController;
destination.fruit = selectedItem;
}
Where fruitsList is an array with all the fruit objects (The one used to show the fruits without any filter), and DestinationViewController is the the controller that will show the picture.
First, why is your entity name in the plural? That is confusing. You should rename your entity Fruit.
Second, how come your variable names start with capital letters? This is again confusing because they can be mistaken for class names. You should rename your variables fruit, such as in fruit.name.
Third, you should use a NSFetchedResultsController to populate your table view. You could have a separate one just for the search or filter in memory. Both are preferable to doing a manual fetch in each call to filterContentForSearchText. Your filter code looks fine (it is the in-memory version). However, I do not see where you reload the data of your search results table view.
Fourth, the search is completely unrelated to the segue problem. Each cell should be associated with a particular Fruit instance. (Either custom cells with a #property of type Fruit, or use the fetched results controller objectAtIndexPath.) In prepareForSegue you just assign that fruit to the detail view controller (which should have an appropriate #property set up).

Optimizing UITableView backed by NSFetcchedResultsController

I recently switched my CoreData backed UITableViews to use a NSFetchedResultsController instead of an NSArray. For one of the tables, scrolling is now very slow, and I think I know why, but I don't know yet what would be the best solution to fix this.
There are two entities Book and Author which are in a many-to-many relationship. Each cell displays the book title, plus the author. If there is more than one author, it will just display the main author. Each Author has an "order" attribute, which is set when the data is imported.
What I have been doing so far is every time the author name is accessed, my Book class returns a mainAuthor property (an NSString):
- (NSString *) mainAuthor
{
if (!mainAuthor)
{
NSSortDescriptor *sortOrder= [NSSortDescriptor sortDescriptorWithKey: #"order" ascending: YES];
NSArray *authorsSorted = [self.authors sortedArrayUsingDescriptors: #[sortOrder]];
Author *a = authorsSorted[0];
mainAuthor = [NSString stringWithFormat: #"%#", a.name];
}
return mainAuthor;
}
For whatever reason this is now called many times instead of only once and causing the slow down. Maybe NSFetchedResultsController fetches the references over and over when scrolling the table?
So how can I fix this? One possibility is to make mainAuthor an attribute instead of a property. So it is set immediately when the data is imported. But before I start messing with my dataModel I'd like to know if this would be the way to move forward, or maybe there is an alternative solution?
UPDATE 1: Here is the code where I set up the fetchController:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSManagedObjectContext *moc = [NSManagedObjectContext MR_defaultContext];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName: #"Book"];
// sort the books by publishing date
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey: #"date" ascending: YES];
[fetchRequest setSortDescriptors: #[sort]];
// only get the books that belong to the library of the current viewController
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"libraries contains[cd] %#", self.library];
[fetchRequest setPredicate: predicate];
[fetchRequest setRelationshipKeyPathsForPrefetching: #[#"authors"]];
[fetchRequest setFetchBatchSize: 10];
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest: fetchRequest
managedObjectContext: moc
sectionNameKeyPath: nil
cacheName: nil];
frc.delegate = self;
_fetchedResultsController = frc;
return _fetchedResultsController;
}
Based on your sample code I'd assume that mainAuthor is not in your Core Data schema. As Core Data handles the lifetime (faulting) of your managed objects you should add this property to your Book entity to avoid unpredictable results by using a transient attribute.
Despite this I'd recommend to return an Author object instead of a NSString as you might change name of the attribute in the future or want to use additional information of the mainAuthor in your UI.
Transient Attribute
Add a transient attribute mainAuthor to your Book's entity and add a custom accessor to your Book's class:
- (Author *)mainAuthor
{
[self willAccessValueForKey:#"mainAuthor"];
Author *value = [self primitiveValueForKey:#"mainAuthor"];
[self didAccessValueForKey:#"mainAuthor"];
if (value == nil)
{
NSPredicate *filterByMinOrder = [NSPredicate predicateWithFormat:#"order == %#.#min.order", self.authors];
value = [[self.authors filteredSetUsingPredicate:filterByMinOrder] anyObject];
[self setPrimitiveValue:value forKey:#"mainAuthor"];
}
return value;
}
The disadvantage of using a transient is that you have to make sure that the data is always up-to-date during the lifetime of the appropriate book. So you have to reset mainAuthor in:
willTurnIntoFault
awakeFromFetch
awakeFromSnapshotEvents:
(Optional, but necessary if the user can change the data)
addAuthorObject:
removeAuthorObject:
by calling [self setPrimitiveValue:nil forKey:#"mainAuthor"].
Hint: Better and faster is to create a synthesized primitiveMainAuthor instead of using primitiveValue:forKey:: Managed Object Accessor Methods
Update
Have you tried to set a fetchBatchSize in your NSFetchedResultsController's fetchRequest? Docs: NSFetchRequest fetchBatchSize
Update 2
Yes, setting the appropriate relationship in setRelationshipKeyPathsForPrefetching is necessary in that case.
To identify bottlenecks it's also really helpful to set the debug argument -com.apple.CoreData.SQLDebug 1 to see the SQL statements created by Core Data. This also often helps to understand the different NSFetchRequest attributes and their impacts.

Updating Core Data Properties WITHOUT using Table Views

I have a POS type app that uses Core Data to store daily sales transactions using table views. I am attempting to retrieve and update certain Core Date Properties, like daily sales counts, WITHOUT using table views. Table views use row at index path to point to the correct object (row). I am using the Fetched Results controller with a predicate to retrieve the fetched object (row) Question: How do I obtain the index of the fetched row so that I can retrieve and then update the correct property values? All books and examples use table views to change properties.
Entity Product
Product *product;
______________________________
[self setupFetchedResultsController]; (This returns one object)
product = [NSFetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; (objectAtIndexPath - Errors of course)
I think you shouldn't use NSFetchedResultsController in this case. If you don't want to use it in either a UITableView or a UICollectionView, you're probably better of without it. You're probably better of using a NSFetchRequest instead, it's pretty easy to set up:
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"someValue=1"];
fetchRequest.predicate = predicate;
NSError *error = nil;
NSArray *array = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
Now you have a NSArray with all the results, which you could use without having to deal with index paths.
If you're still using a NSFetchedResultController for a table (I'm not sure if you do), those rows will still be updated whenever you make a change.
Update: To update one of the objects returned by the fetch, could be done like this:
Entity *entity = [array firstObject];
[entity setSomeProperty:#"CoreDataIsAwesome"];
NSError *error = nil;
if ([self.managedObjectContext save:&error]) {
NSLog(#"Entity updated!");
} else {
NSLog(#"Something went wrong: %#", error);
}
You can use the method indexPathOfObject: on your fetched results controller to return the index path of the given object to then do your updates.

Why do I have to comment out my NSPredicate to have NSFetchedResultsController populate an UITableView?

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;
}
As I told before, I only see results if I uncomment the NSPredicate. I tried with LIKE, ==, =, with double and single quotes around %#...
BTW, The best would be to directly compare the Feed object...
According to Apple, the syntax might not be the issue, but then what?
The Posts are created in a separate ManagedObjectController sharing the same PersistentStoreCoordinator. I get the required Feed's objectID in order to associate the new Post with its corresponding Feed in the child MOC (otherwise I'd get an error regarding associating objects from different MOC).
I also duely merge my MOCs in the main thread whenever the child MOC notifies it of a change.
Basically: if I NSLog the Posts I have (commented-NSPredicate), I see every Post with the relevant RSS Feed URL fitting the displayed Feed (= detailItem).
Anyone can help me?
If your NSFetchedResultsController is blank then it's pretty sure that you're getting no results through the fetch request and that i'm afraid, because of inappropriate predicate statement or no matching records. i guess the problem is due to presence of wildcard(don't know much about that)
check NSPredicate Class Reference and Predicate Programming Guide to get accurate results through predicates.
Eureka!
The problem was the following: when I create my NSFetchedResultsController in my DetailView, _detailItem is nil.
So, even after when setting _detailItem, the NSPredicate still focus on comparing my feed relationship to a nil object.
I solved the problem by refreshing my NSFetchedResultsController.fetchRequest in the didSelectRowAtIndexPath in the MasterView the following way:
Feed *feed;
if (tableView == self.searchDisplayController.searchResultsTableView) {
feed = [_filteredCategoryArray objectAtIndex:indexPath.row];
} else {
feed = [[self fetchedResultsController] objectAtIndexPath:indexPath];
}
self.detailViewController.detailItem = feed;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"feed == %#", feed];
[self.detailViewController.fetchedResultsController.fetchRequest setPredicate:predicate];
Hope this solution might help other people.
Thanks for your help, nickAtStack!

Filtering a UITableView based on an NSPredicate from an NSFetchedResultsController

I'm trying to filter objects in a UITableView, which gets its data from a Core Data sqlite via aNSFetchedResultsController. It's working fine before filtering, but now I need to be able to tap a button and have it display only objects one of whose properties (a BOOL) is YES. Lets call it isFavourite.
I'm a little confused as to where to begin. I understand the concept of a predicate, and I've constructed this one, which should work:
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"isFavourite == 1"];
However, I'm a little confused as to how to attach that predicate to the NSFetchedResultsController. I already allocated and initialised it, using a separate fetch request which simply returned all objects of that entity (and that works perfectly). If it's been initialised already, can I change its fetch request? If so, how do I do that, then how do I make it re-run the query. Do I then need to manually update the UITableView with reloadData or similar. Then, what do I do when I want to switch this filter off and go back to viewing all results?
I've read a lot of existing questions here but most seem to relate to search bars, which make the answers a lot more complicated. I've also read the Apple documentation, but tend to find its written in a pretty cryptic fashion. My code is below, I'm only part-way through the filterFavourite method and that's where I'm looking for help and clarification.
- (NSFetchedResultsController *) fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName: #"Quote" inManagedObjectContext: [[CoreDataController sharedCoreDataController] managedObjectContext]];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey: #"quoteID" ascending: YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize: 50];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest: fetchRequest managedObjectContext: [[FQCoreDataController sharedCoreDataController] managedObjectContext] sectionNameKeyPath: nil cacheName: nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
- (IBAction) filterFavourite: (id) sender
{
if (isDisplayingFavourite) {
// Switch to showing all..
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"isFavourite == 1"];
isDisplayingFavourite = NO;
} else {
// Switch to showing favourites only..
isDisplayingFavourite = YES;
}
}
EDIT: The delegate method "controllerDidChangeContent" is never called in this code. I can't figure out why that is, either. Hugely, hugely confusing, Core Data is like a brick wall. In fact, NONE of the four delegate methods are ever called.
You don't attach a predicate to the NSFetchedResultsController-- you attach it to the NSFetchRequest. It has a method called setPredicate: that does exactly what its name suggests.
You can modify the fetch request by updating the fetchRequest attribute and calling performFetch: again. You could also nil out your fetchedResultsController ivar, and make the code above flexible enough to add whatever predicate(s) would be useful. The next time your fetchedResultsController method is called, it will create a new instance for you with the current predicate(s). That consolidates all of your NSFetchedResultsController code in one method. It's probably slightly more expensive in CPU usage but also probably easier to maintain.

Resources