I have been looking over this issue since a week and haven't got any solution, so I thought to make the question more generalized, may be it will help the users to look into it and give me a solution.
Scenario:
I have an expense tracking iOS Application and I have a view controller called "DashBoardViewController" (table view controller - with FRC) which would basically categorize my expenses/incomes for a given week, a month, or year and display it as the section header title for example : (Oct 1- Oct 7, 2012) and it shows expenses/incomes ROWS and related stuff according to that particular week or month or year.
My Question:
What I want to accomplish is :
Show Distinct Results based on "Category" attribute of "Money" entity and calculate "Sum" based on the attribute.
But, my view controller called "dashboard view controller" is filled with NSFetchedResultsController with section name key path as "type" that can be an expense or an income. In order to get Distinct results, I shall use Result type as NSDictionaryResultsType in my fetch request which will give me unique results but FRC fails, it doesn't work with that. So, how will I get my section names then? I have posted the code below.
EDIT - BASED ON MARTIN'S SUGGESTION
- (void)userDidSelectStartDate:(NSDate *)startDate andEndDate:(NSDate *)endDate
{
AppDelegate * applicationDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [applicationDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Money" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
NSPredicate *predicateDate = [NSPredicate predicateWithFormat:#"(date >= %#) AND (date <= %#)", startDate, endDate];
[fetchRequest setPredicate:predicateDate];
// Edit the sort key as appropriate.
typeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"type" ascending:YES];
dateSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
if(self.choiceSegmentedControl.selectedIndex == 0)
{
choiceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"cat" ascending:NO];
}
if(self.choiceSegmentedControl.selectedIndex == 1)
{
choiceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"vendor" ascending:NO];
}
if(self.choiceSegmentedControl.selectedIndex == 2)
{
choiceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"paidBy" ascending:NO];
}
NSArray * descriptors = [NSArray arrayWithObjects:typeSortDescriptor, dateSortDescriptor,choiceSortDescriptor, nil];
[fetchRequest setSortDescriptors:descriptors];
[fetchRequest setIncludesSubentities:YES];
NSError * anyError = nil;
NSArray *propertiesToFetch = [[NSArray alloc] initWithObjects:
[entity.propertiesByName objectForKey:#"cat"],
nil];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setResultType:NSDictionaryResultType];
[fetchRequest setPropertiesToFetch:propertiesToFetch];
NSArray * objects = [context executeFetchRequest:fetchRequest error:&anyError];
for (NSDictionary *d in objects)
{
NSLog(#"NSDictionary = %#", d);
}
NSSet *uniqueSet = [NSSet setWithArray:[objects valueForKey:#"cat"]];
uniqueArray = [NSMutableArray arrayWithArray:[uniqueSet allObjects]];
self.categoryArray = uniqueArray;
if(_fetchedResultsController)
{
[_fetchedResultsController release]; _fetchedResultsController = nil;
}
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:#"type" cacheName:nil];
//self.fetchedResultsController.delegate = self; in order to stop "change tracking"
if(![_fetchedResultsController performFetch:&anyError])
{
NSLog(#"error fetching:%#", anyError);
}
[fetchRequest release];
//Finally you tell the tableView to reload it's data, it will then ask your NEW FRC for the new data
[self.dashBoardTblView reloadData];
self.startDate = startDate;
self.endDate = endDate;
}
With this code, SECTION NAME KEY PATH is not working and it's complaining that the object will be placed in unnamed section.
A fetched results controller does not support change tracking (i.e. the FRC delegate is set) in combination with a fetch request with NSDictionaryResultType.
The reason is documented with the setIncludesPendingChanges: function:
Special Considerations
A value of YES is not supported in conjunction
with the result type NSDictionaryResultType, including calculation of
aggregate results (such as max and min). For dictionaries, the array
returned from the fetch reflects the current state in the persistent
store, and does not take into account any pending changes, insertions,
or deletions in the context.
Change tracking of the FRC implies includesPendingChanges = YES for the fetch request, and that does not work with NSDictionaryResultType.
One workaround could be to use a FRC without change tracking, so you do not set the FRC delegate. But that means that to update your table view, you have to
save the managed object context, and
call performFetch on the FRC and reloadData on the table view.
Another workaround could be to use the FRC to fetch all sections and rows without the sum aggregation, and use the results to compute new table rows with the aggregation in memory (e.g. in controllerDidChangeContent).
UPDATE: (From the discussion) Another important point is that if you use a fetch request with NSDictionaryResultType, then the sectionNameKeyPath of the fetched results controller must be included in the propertiesToFetch of the fetch request.
Related
I'm fairly new to Core Data and am still trying to understand accessing and filtering related data. My problem is either I'm not getting the data correctly into the managedObjectContext or I'm not pulling it out correctly. (I think the first, but I'm not sure.)
Here's my data model with two entities related one to many: (I plan to refactor once I get one level of relationship working.)
I have a SeasonsVC in which you click on a season name and the list of games for that season is supposed to appear in the GamesVC and you have the option to add or edit an existing game. This works fine at a first pass. I can add and edit games via this code in the GameDetailsVC:
-(IBAction)done:(UIBarButtonItem *)sender {
Game *game = nil;
if (self.gameToEdit != nil) {
game = self.gameToEdit;
NSLog(#"Hitting gametoedit");
} else {
game = [NSEntityDescription insertNewObjectForEntityForName:#"Game" inManagedObjectContext:self.managedObjectContext];
NSLog(#"Hitting new game");
}
game.opponent = self.opponentTextField.text;
//season.seasonDescription = self.seasonDescriptionTextView.text;
NSLog(#"Game to edit: %#", game.opponent);
//NSLog(#"Season: %#", season);
//NSLog(#"MOC in done: %#", self.managedObjectContext);
//NSLog(#"Season name: %#", season.seasonName);
[self dismissViewControllerAnimated:YES completion:nil];
}
I can then see the games in the GamesVC via the fetchedResultsController and delegate methods, but each game is associated with every season. Once I try to filter the data with a predicate so that I only see the games that were added for that season all the games disappear. Here's the code for that from the GamesVC:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Game" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSLog(#"Season name for predicate %#", self.season.seasonName);//shows correct season name
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"season.seasonName like '%#'", self.season.seasonName]];
[fetchRequest setPredicate: predicate];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"dateOfGame" ascending:NO] ;
[fetchRequest setSortDescriptors:#[sortDescriptor]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil
cacheName:#"Root2"];
self.fetchedResultsController = theFetchedResultsController;
self.fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Since I can log the correct season name right before the predicate statement, I think that the added games are not getting "associated" with the correct season when I put them in the MOC in the done: method shown above; otherwise, I'd expect the predicate to find them.
Can you help this rookie? Thanks.
A day later I have this sorted. I needed to make three changes to what I was doing.
First, in my prepareForSegue in my GameListVC I needed to pass the season along to the GameDetailsVC by changing the code to this. (Just added the one line: controller.season = self.season)
//pass the Season object to be edited
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
Game *gameToEdit = [self.fetchedResultsController objectAtIndexPath:indexPath];
controller.gameToEdit = gameToEdit;
controller.season = self.season;
Second, I had to put the new/edited game object into the MOC by adding these two lines to my done: method in the GameDetailsVC.
NSMutableSet *games = [self.season mutableSetValueForKey:#"games"];
[games addObject:game];
Third, I needed to simplify my predicate statement in my fetchedResultsController method in the GameListVC.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"season.seasonName like %#", self.season.seasonName];
So, to answer my own question, I was both not saving the game linked to the season and my predicate was wrong.
I hope some of this helps another rookie.
I have Core Data with +2 millions of rows and i want to search for two specific fields: name & phone (for example). I insert scopes for each field in the Search Bar. Everything go fine if I haven't large data set. That's why I want to search in my core data without load all rows in memory before go search controller. Just need a result when my search text length > 3 or when the Search Button Clicked.
I have just one Table View with Search Bar
I populate when AppDidFinish with Call history plist
When the search bar isActive my App frezes until 2 millions rows loads. I need Avoid this step and move forward to step 4
Enter the search text. Then the result filteredArray has shown in the Table View
...
If have any idea I will appreciated.
Here you have some codes:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Guia" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:50];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"GuiaCache"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
abort();
}
return _fetchedResultsController;
}
Maybe in this method I need to improve with some Predicate
- (NSFetchRequest *)searchFetchRequest
{
if (_searchFetchRequest != nil) {
return _searchFetchRequest;
}
_searchFetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Guia" inManagedObjectContext:self.managedObjectContext];
[_searchFetchRequest setEntity:entity];
[_searchFetchRequest setFetchBatchSize:50];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[_searchFetchRequest setSortDescriptors:sortDescriptors];
return _searchFetchRequest;
}
Finaly the two functions to search in Core Data
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
if ([searchText length] > 3)
[self searchForText:searchText scope:_scopeKeyIndex];
}
- (void)searchForText:(NSString *)searchText scope:(NSUInteger)scopeOption
{
if (self.managedObjectContext)
{
NSString *predicateFormat = #"%K BEGINSWITH[cd] %#";
NSString *searchAttribute = #"telephone";
if (scopeOption == 1)
{
searchAttribute = #"name";
}
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat, searchAttribute, searchText];
[self.searchFetchRequest setPredicate:predicate];
NSError *error = nil;
_filteredList = [self.managedObjectContext executeFetchRequest:self.searchFetchRequest error:&error];
}
}
If you need some other piece of code, just comment.
First, run Instruments against your application, specifically Time Profiler. Where does it say the time is being spent?
Do you have a predicate in your search? What does the predicate look like? You need to show some code for people to help.
Doing a fetch against against 2 million records will take a bit of time but I suspect it is not loading 2 million rows into memory as that would most likely cause memory problems as well as speed problems.
Post the results from Time Profiler and lets see where the time is being spent.
Update
First your predicate is very expensive. BEGINSWITH should be avoided if at all possible. Making it both case and diacritic insensitive increases that cost dramatically.
Did you run Instruments against your code? Did you run the time profiler? Without running that you are just guessing. You NEED to run the time profiler and at least show the results if not link to the entire profile.
I am really having trouble retrieving items that have been created through the Menu entity. This is the code I used to add an item to a specific Menu object
-(void)additem:(NSString *)entity :(NSDictionary *)aDictionary :(NSString *)menu
{
NSLog(#"additem");
NSError *error = nil;
Menu *menuItem = nil;
NSFetchRequest * request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Menu" inManagedObjectContext:managedObjectContext]];
[request setPredicate:[NSPredicate predicateWithFormat:#"mname=%# and msection =%#",#"Parents",#"Keydates"]];
menuItem = [[managedObjectContext executeFetchRequest:request error:&error] lastObject];
if (error) {
//Handle any errors
}
if (!menuItem) {
//Nothing there to update
NSLog(#"This class doesn't exist");
}
Items *anitem = (Items *)[NSEntityDescription insertNewObjectForEntityForName:#"Items" inManagedObjectContext:managedObjectContext];;
anitem.type = [aDictionary objectForKey:#"type"];
anitem.title = [aDictionary objectForKey:#"title"];
anitem.image = [aDictionary objectForKey:#"image"];
anitem.subtitle = [aDictionary objectForKey:#"subtitle"];
[menuItem addItemsObject:anitem];
[managedObjectContext save:&error];
}
I want to use a predicate to retrieve all the items that were created on a specific Menu object. Here is the code I am trying to retrieve it with.
- (void) readItems: (NSString *)section {
NSLog(#"readItems");
NSError *error = nil;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Menu"
inManagedObjectContext:managedObjectContext];
fetchRequest.resultType = NSDictionaryResultType;
[fetchRequest setEntity:entity];
[fetchRequest setReturnsObjectsAsFaults:NO];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"mname=%# and msection = %#",#"Parents",#"Keydates"]];
NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
for (Items *item in fetchedObjects) {
NSLog(#">>>>>>%#",item);
}
}
Can anyone point me in the right direction.I know I pass section and don't use it. I have place the actual values in.
Your readItems: method specifies the entity for your fetch as Menu yet when you execute the fetch you are expecting Item (since you start iterating through fetchedObjects and casting them as Item).
Instead what you want to do is et your entity to Item and change the predicate to
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"items.mname=%# and items.msection = %#",#"Parents",#"Keydates"]];
Furthermore you should consider renaming your relationships. The relationship field specifies exactly what you would expect to see when you follow the arrow. Thus when I look at Items and follow its arrow to Menu I would expect the name of this relationship to be menu yet you call it menuItems. You have done this correctly for the relationship from Menu to Item.
That way we would end up with a more readable predicate looking like:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"menu.mname=%# and menu.msection = %#",#"Parents",#"Keydates"]];
Happy Coding
I know I shouldn't look at Core Data as something relational. But with a SQL query it is easiest to explain what I'm trying to do.
What I'd like to do is get the max value from a property constrained to the children of an entity. In SQL something like: select max(valueY) from graphdata where parentID = 10
I'm doing something akin to the above SQL query successfully excluding the where (NSPredicate) part.
I added the NSPredicate to my code thinking it would exclude the graphdata objects not within the KPI I'm looking at. Well, guess not, it's not what I'm seeing, it seems to do nothing at all really.
The Core Data Model consistst of multiple KPI objects, each containing an Array of GraphData objects. Every GraphData object contains a valueX and a valueY property.
I want the max valueY for the graphdata of a specified KPI object.
Here's my code, it's based on one of Apple's core data snippets:
NSManagedObjectContext *context = ...
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:GRAPH_DATA_ENTITY_NAME inManagedObjectContext:context];
[request setEntity:entity];
// Specify that the request should return dictionaries.
[request setResultType:NSDictionaryResultType];
// Create an expression for the key path.
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"ValueY"];
//MY WHERE KIND OF CLAUSE/PREDICATE WHICH ISN'T WORKING?
NSPredicate *predicate =
[NSPredicate predicateWithFormat:#"kpiForGraphData == %#", self.kpi];
[request setPredicate:predicate];
// Create an expression to represent the function you want to apply
NSExpression *expression = [NSExpression expressionForFunction:#"max:"
arguments:[NSArray arrayWithObject:keyPathExpression]];
// Create an expression description using the minExpression and returning a date.
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
// The name is the key that will be used in the dictionary for the return value.
[expressionDescription setName:#"maxValueY"];
[expressionDescription setExpression:expression];
[expressionDescription setExpressionResultType:NSDecimalAttributeType]; // For example, NSDateAttributeType
// Set the request's properties to fetch just the property represented by the expressions.
[request setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
// Execute the fetch.
NSError *error;
id requestedValue = nil;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
// Handle the error.
}
else {
if ([objects count] > 0) {
requestedValue = [[objects objectAtIndex:0] valueForKey:#"maxValueY"];
}
}
[expressionDescription release];
[request release];
return [requestedValue doubleValue];
So far I've been trying to help myself by reading docs, searching through Google and stackoverflow. Everything I seem to find either selects entities (NSPredicates) or selects values using a function. Never have I seen an example or an explanation doing what I'd like to do. I like Core Data, but right now SQL seems way more straightforward to me.
I have a one to many entity relationship between two entities:
EntityP (Parent) <-->> EntityC (Child)
Attributes and Relationships:
EntityP.title
EntityP.dateTimeStamp
EntityP.PtoC (relationship)
EntityC.title
EntityC.dateTimeStamp
EntityC.CtoP (relationship) // Can be used to get "one" EntityP record
I use fetch results controller to show the results. Here's my implementation of the fetch results controller:
#pragma mark -
#pragma mark Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController {
// Set up the fetched results controller if needed
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create the fetch request for the entity
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Set Entity
NSEntityDescription *entity = [NSEntityDescription entityForName:#"EntityC" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set Predicate
// (Ignore, we want to get list of all EntityC records)
// Set Sort Descriptors (sort on Parent - for section, and then on Child - for rows)
NSSortDescriptor *sortDescriptorPDate = [[NSSortDescriptor alloc] initWithKey:#"CtoP.dateTimeStamp" ascending:YES];
NSSortDescriptor *sortDescriptorDate = [[NSSortDescriptor alloc] initWithKey:#"dateTimeStamp" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptorPDate, sortDescriptorDate, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize:20];
// Create and initialize the fetch results controller
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"CtoP.title" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
// Cleanup memory
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptorPDate release];
[sortDescriptorDate release];
[sortDescriptors release];
NSError *error = nil;
if (![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;
}
Now, for example, if i have following data in the persistence store:
EntityP.dateTimeStamp EntityP.title EntityC.dateTimeStamp EntityC.title
Today B Today d
Yesterday A Yesterday a
Today B Yesterday c
Yesterday A Today b
Note: Yesterday and Today is in NSDate format.
Then i should get the sections and rows in following order (exactly):
A
a
b
B
c
d
But, the sort is not working like this. I'm getting the rows in correct order, but the sections are not ordered! I hope sortDescriptorPDate is doing his job. What am i missing? Thanking in anticipation.
Not sure I understand your setup but...
By default the section titles are the capitalized first letter of the section name. If you want a custom sections such as ones based on dates, you will need to subclass NSFetchedResultsController and override the various sectionIndex... methods to return the correct sections, section indexes and section titles.
This seems to be working now! I'm not sure what changed! – Mustafa Sep 6 '10 at 4:03