GROUP BY equivalent predicate for NSFetchedResultsController - iphone

I'm trying to get a proper NSFetchedRequest to query the next unwatched episodes for a given TV-Show.
It should be put together in one request because I have to hand it to a NSFetchedResultsController.
My Model (CoreData):
Show
- Seasons:NSSet<Show>
Season
- Episodes:NSSet<Episode>
Episode
- first_aired:NSDate
- watched:BOOL
There should be 1 (or 0) Episode returned for every show.
I can put this together with multiple requests.
for example:
I set the NSFetchedResultsController up to fetch every show.
in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath i iterate over every Show to get the latest episode
+(Episode *)nextAiringEpisodeForShow:(Show *)show{
NSTimeInterval timeinterval = 0.0;
NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeinterval];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Episode"];
request.predicate = [NSPredicate predicateWithFormat:#"season.show = %# AND watched = false AND season.seasonNumber > 0 AND first_aired > %#",show,date];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"first_aired" ascending:NO];
request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
return [[show.managedObjectContext executeFetchRequest:request error:nil] lastObject];
}
The problem doing it this way:
There are Shows for which one has already watched all the episodes.
These will be displayed as (null) in the UITableView and there is no way to remove them from the UITableView because the NSFetchedResultsController is set as it's dataSource.
Furthermore there is no way to sort the TableView by first_aired when doing it this way.
Or am I missing something here?!

Instead of fetching all shows in the fetched results controller, you could fetch only shows for which there is at least one unwatched episode. This should work with the following predicate (for the fetch request on "Show"):
[NSPredicate predicateWithFormat:#"SUBQUERY(seasons, $x, ANY $x.episodes.watched == FALSE).#count > 0"]

Related

removing an object from a fetchedResultsController fetchedObjects

Is it possible to remove an object from a fetchedResultsController fetchedObjects?
For example I have the following code:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"pack.packName IN %#", allowedPackNames];
for (int i = 0; i < [tempFetchResults.fetchedObjects count]; i++){
Author *author = [tempFetchResults.fetchedObjects objectAtIndex:i];
NSSet *filteredQuotes = [author.quotes filteredSetUsingPredicate:predicate];
if ([filteredQuotes count] > 0){
author.quotes = filteredQuotes;
} else {
//remove this author from the fetchedObjects array
}
}
How can I do this?
To remove the object from the data store:
[self.managedObjectContext deleteObject:object];
[self.managedObjectContext save:nil];
[self.fetchedResultsController self.fetchedResultsController.fetchRequest];
[tableView reloadData];
To just remove the object from the fetched results controller array, you need to change the predicate. Make sure you disable cache (or change the cache name) for this to work.
I think you could define a property 'excluded' or something like that of integer type and incorporate that property into your fetch request predicate. Now when you mark a quote as excluded, fetched results controller will do all the heavy lifting for you without refetching and all.
Update #1
Addressing the bigger issue here I think using subquery in your fetched results controller predicate is much better idea.
Try using:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SUBQUERY(quotes, $x, $x.pack.packName IN %#).#count > 0", allowedPackNames];
as a predicate for your fetch request for fetched results controller and forget about manual filtering.
Basically what this subquery should do for you is filter out all authors that have quotes count fitting your packName criteria. I admit I did not test this query. If you say this won't work for you I will go through the trouble and test it myself.

Dynamically setting fetchLimit for NSFetchedResultsController

I'm using am NSFetchedResultsController to populate data onto a UITableView.
It's a simple chat app and I want to load the latest 25 messages onto the table first and load more as the user scrolls up to see older messages (the chat message are in a ascending order).
I call a method that will setFetchLimit: for the NSFetchedResultsController in the willDisplayCell: like so....
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == 0)
{
[self performSelector:#selector(getMoreMessages) withObject:nil afterDelay:1.0];
}
}
when the first row of the UITableView has been displayed, getMoreMessages will try to reset the fetchLimit reload the UITableView like so.....
- (void)getMoreMessages
{
maxListItems += 25;
NSLog(#"set maxListItems: %d", maxListItems);
[self.resultsController.fetchRequest setFetchLimit:maxListItems];
[self._tableView reloadData];
}
However, it doesn't seem to be working, the table data will not change.
The initial NSFetchRequest is set like so...
NSFetchRequest *chatDataRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"ChatData" inManagedObjectContext:appDelegate.managedObjectContext];
[chatDataRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(key != 0 OR messageNo != 0) and matchNo = %d", matchNo];
[chatDataRequest setPredicate:predicate];
NSSortDescriptor *sortDescripter1 = [[NSSortDescriptor alloc] initWithKey:#"status" ascending:YES];
NSSortDescriptor *sortDescripter2 = [[NSSortDescriptor alloc] initWithKey:#"messageNo" ascending:YES];
NSArray *sortDescripters = [[NSArray alloc] initWithObjects:sortDescripter1, sortDescripter2, nil];
[chatDataRequest setSortDescriptors:sortDescripters];
[sortDescripters release];
[sortDescripter1 release];
[sortDescripter2 release];
[chatDataRequest setFetchLimit:25];
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:chatDataRequest managedObjectContext:appDelegate.managedObjectContext sectionNameKeyPath:nil cacheName:[NSString stringWithFormat:#"%d_chat.cache", matchNumber]];
[chatDataRequest release];
fetchedResultsController.delegate = self;
NSError *error;
BOOL success = [fetchedResultsController performFetch:&error];
if(!success) NSLog(#"error: %#", error);
self.resultsController = fetchedResultsController;
And back to the question.
How can one dynamically change the fetchLimit for an NSFetchedResultsController?
Any hits would be awesome!
Thanks!
Instand using setFetchLimit, using setBatchSize, see below for detail answer.
The count of the fetchedObjects array might not what you want to do, since it does not update the changes from the persistent store. From NSFetchedResultsController documentation:
The results array only includes instances of the entity specified by the fetch request (fetchRequest) and that match its predicate. (If the fetch request has no predicate, then the results array includes all instances of the entity specified by the fetch request.)
The results array reflects the in-memory state of managed objects in the controller’s managed object context, not their state in the persistent store. The returned array does not, however, update as managed objects are inserted, modified, or deleted.
If you only want to fetch 20 objects, set the fetch limit of the NSFetchRequest. If you want only to keep 20 objects in memory, use setBatchSize of the NSFetchRequest object.
figured this one out.
looks like I have to run performFetch: after I change the fetchLimit. :D
[self.resultsController.fetchRequest setFetchLimit:maxListItems];
[self.resultsController performFetch:&error];

Core Data, Search Controller and Table View

I have integrated the search function to my app to query core data/.sqlite and it works ok. But I have a problem and I am not sure which class configuration should I look at, could someone points me to the light, thanks
Basically my model is this
TableView 1
Display Product Category
selectRow --> TableView2
TableView 2
Display Products of selected Category
selectRow --> TableView3
As I integrated the UISearchBar in TableView 1, I wish to have the function when people search the product they want and it will show up the product's name right away in the table view. I tried, but the result is showing the Category which contains the "searched product".
So, how could I get this to show up correctly and which section of configuration should I look at?
UISearchDisplayController *searchDisplayController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
[self performSelector:#selector(setSearchDisplayController:) withObject:searchDisplayController];
[searchDisplayController setDelegate:self];
[searchDisplayController setSearchResultsDataSource:self];
[searchDisplayController setSearchResultsDelegate:self];
[searchDisplayController release];
[self.tableView setContentOffset:CGPointMake(0,self.searchDisplayController.searchBar.frame.size.height)];
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();
self.filteredListContent = [NSMutableArray arrayWithCapacity:[[[self fetchedResultsController] fetchedObjects] count]];
}
Is it this part of the code?
Thanks
Update with more info:
Configure Cell
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
NSManagedObject *entity = nil;
if (self.searchIsActive){ // Configure the cell to show the searched item's name
entity = [[self filteredListContent] objectAtIndex:[indexPath row]];
cell.textLabel.textColor = [UIColor blackColor];
} else {// Configure the cell to show the category's name
cell.textLabel.textColor = [UIColor blackColor];
entity = [fetchedResultsController objectAtIndexPath:indexPath];
}
cell.textLabel.text = [entity valueForKey:#"nameEN"];
}
The Search Predicate
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"ANY products.nameEN CONTAINS[cd] %#", searchText];
self.filteredListContent = [[[self fetchedResultsController] fetchedObjects] filteredArrayUsingPredicate:predicate];
}
The Core Data Structure
Category{
nameEN
products <- one to many relation ->> Product.productcat
}
Product{
nameEN
spec
productcat <<-- many to one relation-> Category.products
}
Thank you.
If your data model has a Product entity and a Category entity and your fetches are returning Category objects instead of Product objects, then you have the wrong entity set for your fetch.
[ignore following as it applies to a different type of search -- techzen]
You usually create a separate fetch for the search because every time the users enters new characters in the search, the predicate for the fetch must change.
Update:
Okay, I misunderstood the type of search you were implementing. You are filtering the return of an existing fetch instead of fetching based on the entered search.
Looking at the predicate and data model in your update I think it clear that the predicate will only work against an array of Category objects. This:
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"ANY products.nameEN CONTAINS[cd] %#", searchText];
... can only filter Category objects because only the Category object has an attribute of products. This predicate says, "Match all Catergory objects in which any related Product object has a nameEn attribute value that contains the search text."
Remember as well that the array you are filtering here:
self.filteredListContent = [[[self fetchedResultsController] fetchedObjects] filteredArrayUsingPredicate:predicate];
...is an array of Category objects and not Product objects.
I think you need to rethink your UI design. Your TableView1 defaults to displaying a list of Category objects but you want your search of that table to display a list of Product objects. That will confuse the user. The user will expect a search on a table of Category objects to return a subset of Category objects.
However, with the existing design, you can produce an array of Product objects with the current code by creating a new array of Product objects by apply the #distinctUnionOfObjects collection operator:
self.filteredListContent = [[[self fetchedResultsController] fetchedObjects] filteredArrayUsingPredicate:predicate];
NSArray *distinctProducts=[self.filteredListContent valueForKey:#"products.#distinctUnionOfObjects.enName"];
... distinctProducts will now be an array of Product objects matching the search criteria. Use that array in configureCell:atIndexPath (you may need to resort it.)
So, I tried these at the configure cell part
NSDictionary *distinctProducts=[self.filteredListContent valueForKey:#"products"];
NSLog(#"what are products:%#",distinctProducts);
NSArray *listofProductsName = [distinctProducts valueForKey:#"nameEN"];
NSLog(#"whatup: %#",listofProductsName);
NSArray *entity = [listofProductsName objectAtIndex:indexPath.row];
cell.textLabel.textColor = [UIColor blackColor];
cell.textLabel.text = entity;
But then I could convert the name to show...it said _NFSet isEqualToString: ....etc terminated NSException.. though the NSLog of listofProductsName show up the right products name list.

NSFetchRequest / Predicate Question

I have two entities: Patient and Job. Patient has a to-many relationship to Job called 'jobs' and Job has a to-one relationship to Patient called 'patient'. Job has attributes called 'dueDate' (Date) and 'completed' (BOOL) and Patient has attributes 'firstName' and 'lastName' (both Strings).
I am trying to create a fetch request / predicate for my NSFetchedResultsController that we grab all Jobs that have not been completed (i.e. completed == NO) and section them by Patient name. Here is my code:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Job" inManagedObjectContext:moc];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(completed == NO)"];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *patientDescriptor = [[NSSortDescriptor alloc] initWithKey:#"patient" ascending:YES];
NSSortDescriptor *dueDateDescriptor = [[NSSortDescriptor alloc] initWithKey:#"dueDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:patientDescriptor, dueDateDescriptor, nil];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:moc sectionNameKeyPath:#"patient" cacheName:#"Jobs"];
Here is my titleForHeaderInSection method:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:section];
NSString *firstName = [[(Job *)[fetchedResultsController objectAtIndexPath:indexPath] patient] firstName];
NSString *lastName = [[(Job *)[fetchedResultsController objectAtIndexPath:indexPath] patient] lastName];
return [NSString stringWithFormat:#"%# %#", firstName, lastName];
This doesn't seem to work. Am I going about this the wrong way?
How is it not working? It helps to describe what results you are seeing.
You are not adding your sort descriptors to your NSFetchRequest, at least in the sample you provided.
Your sort descriptors are ineffective. It appears that Patient is a relationship so sorting against the relationship will not work. You would want to do a sort like the following:
NSSortDescriptor *lastNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"patient.lastName" ascending:YES];
NSSortDescriptor *firstNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"patient.firstName" ascending:YES];
NSSortDescriptor *dueDateDescriptor = [[NSSortDescriptor alloc] initWithKey:#"dueDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: lastNameDescriptor, firstNameDescriptor, dueDateDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[lastNameDescriptor release], lastNameDescriptor = nil;
[firstNameDescriptor release], firstNameDescriptor = nil;
[dueDateDescriptor release], dueDateDescriptor = nil;
You do not need to cast [fetchedResultsController objectAtIndexPath:indexPath] as it returns id.
What are you getting back from the call to [fetchedResultsController objectAtIndexPath:indexPath]? Put a breakpoint here and check the value and make sure you are getting back a NSManagedObject instead of nil. Putting a breakpoint in that method will also confirm that you are getting called.
Your secondKeypathName will not work as mentioned above. You probably want to set it to #"patient.lastName" so that it will match the initial sort I described above.
Your -tableView: titleForHeaderInSection: should be accessing the cache provided by the NSFetchedResultsController instead of assuming that there is going to be a row in the section:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
Finally, if you want the section to truly display the "lastname, firstname" format then you will need to create a non-transient derived value property on your Patient entity that is the fullName so that you can create your cache based on it. This derived value would then need to be updated any time that the first name or last name were changed.
First, you don't seem to attach the sortDescriptors to fetchRequests. This may or may not be connected with the problem.
Second, you can accomplish this in an easier way. Make it like this:
sectionNameKeyPath:#"patient.name"
"name" should be a property or method of the Patient object. An easy way to implement this would be to have a category method on patient:
- (NSString *)name {
return [NSString stringWithFormat:#"%# %#", self.firstName, self.lastName];
}
Actually, you can't accomplish your purpose with something as simple as this, read mzarra's answer for correct answer. NSFetchedResultsController has this critical comment:
If the controller generates sections, the first sort descriptor in the array is used to group the objects into sections; its key must either be the same as sectionNameKeyPath or the relative ordering using its key must match that using sectionNameKeyPath.
But, you cannot sort on the results of a method call, you'd need a property of the object. So, your best bet is probably just have a "name" property on "patient", and use that property for both sorting and sectionNameKeyPath.
In addition to not assigning your sortDescriptors to your fetchRequest, I believe you have a problem with your predicate. Since you are dealing with Core Data, the boolean value for your "completed" attribute is stored in an instance of NSNumber. Something like the predicate below would be better:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"completed = %#", [NSNumber numberWithBool:NO]];

How do I get Attributes from Core Data into an Array for - iPhone SDK

I'm trying to retrieve data from Core Data and put it into a Mutable Array
I have an Entity called 'Stock' and in Properties, attributes called : code, price & description...
How do I get the data stored in these attributes into a simple Mutable Array?
I've added this code...
NSMutableArray *array = [[NSMutableArray alloc]init];
[array addObject:[stock valueForKey:#"code"]];
and I get this error...
'-[NSCFArray insertObject:atIndex:]: attempt to insert nil'
I have a 'Managed Object Class' called 'Stock' and declared called stock. Am I missing something?
If I do this in the -cellForRowAtIndexPath...
Stock *stock1 = [fetchedResultsController objectAtIndexPath:indexPath];
array = [[NSMutableArray alloc] init];
[array addObject:stock1.code];
NSLog(#"Filtered List is? %#", array);
In the console I can see these 2 items
'The Filtered array is 810005'
'The Filtered array is 810007
'
What must I do to get these items(810005 & 810007) into an array set up in the -viewDidLoad method? Like it does in the -cellForRowAtIndexPath?
Update
Hi Marcus,
Finally got it working (well, 80%)
I put this in the -cellForRowAtIndexPath
Stock *product = nil;
if (tableView == self.searchDisplayController.searchResultsTableView)
{
filteredListContent = [NSMutableArray arrayWithObjects:stock1.code, nil];
product = [self.filteredListContent objectAtIndex:indexPath.row];
[self configureFilteredCell:cell atIndexPath:indexPath];
[filteredListContent objectAtIndex:indexPath.row];
NSLog(#"Filtered List Array List is? %#", stock1.code);
}
else
{
listContent = [NSMutableArray arrayWithObjects:stock1.code, nil];
[self configureCell:cell atIndexPath:indexPath];
NSLog(#"List Array List is? %#", stock1.code);
}
Then I used this code in the scope
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
self.savedSearchTerm = searchText;
if (searchText !=nil)
{
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"code beginsWith[cd] %#", searchText];
[fetchedResultsController.fetchRequest setPredicate:predicate];
}
else
{
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"code contains[cd] %#", searchText];
[fetchedResultsController.fetchRequest setPredicate:predicate];
[self.tableView reloadData];
}
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error])
{
// Handle error
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
[self.tableView reloadData];
Everything is filtering fine but when I hit cancel on the search, it's not reloading the original data...
I won't be defeated...!!
Thanx
Since you are having this issue in your -viewDidLoad, I am guessing (and without the code from -viewDidLoad, it is only a guess) that you are trying to fetch objects from the NSFetchedResultsController before the -executeFetch: has been called on the controller and therefore you are in the land of nils.
I would suggest setting a break point in your -viewDidLoad and watching the values and you walk through your code. This will tell you what is nil and where.
Of course a better question is, why are you trying to put NSManagedObject instances into a NSMutableArray? Since they are already in your NSFetchedResultsController is there really a need to build up another array? What is the end goal?
Update
Now I understand what you are trying to do.
Solution 1
Only populate the array when a search has been conducted. Take a look at the http://developer.apple.com/iphone/library/samplecode/TableSearch/index.html example code and you should see how to apply it to your situation.
If you want to enter the table view with a pre-defined search then you need to perform it after you have executed a -performFetch: in the NSFetchedResultsController.
Solution 2
Modify the NSPredicate on the NSFetchedResultsController to include your search terms and then execute -performFetch: on the NSFetchedResultsController, you may have to do a -reloadData on the table as well, I am not sure.
When the user clears the search field you reset the predicate and re-fetch everything. Since it is all cached there should be no performance penalty.
Solution 2 just occurred to me and I have not tested it personally but there is no reason it shouldn't work just fine. Should even give you live updates within the search.
Have you read the documentation? You fetch your Stock instances (all of them or filter them with a predicate), then do with them whatever you please.
You can then add their properties to an array individually:
[array addObject:[stockInstance valueForKey:#"price"];
... or use a combination of < NSKeyValueCoding > protocol methods such as -dictionaryWithValuesForKeys: NSDictionary methods such as -objectsForKeys:notFoundMarker: to get an array for given keys.
This may or may not actually be what you need to do, though. It depends on what you intend to use the resulting array for. If you want a quick sum of all matching Stock instances' "price" values, for example, you can use Set and Array Operators. It really depends on what you're trying to achieve.
When I got your error,
'-[NSCFArray insertObject:atIndex:]: attempt to insert nil'
I had given the fetchedRequest a sort descriptor that had a nil key. The error appeared when I used these lines:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:nil ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
The error disappeared when I set the key to #"name":
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];