Search code examples
core-datanspredicatensfetchedresultscontrollernsfetchrequest

NSFetchedResultsController with NSDictionaryResultsType in NSFetchRequest


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.


Solution

  • 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.