0

I've been struggling with this now for hours and days. Some stuff in my view controllers is immediately updated when changes occur and some isn't. As an example, I'll try to copy as much code as possible for you to understand.

I'm building a budget app for my company. Three entities are: MainCategory, SpendingCategory and Expenditures. A spending category has several expenditures and a main category several spending categories.

MainCategory <-->> SpendingCategory <-->> Expenditures

Edit and adding transactions happens in the TransactionViewController. As a model I have a SpendingCategory. You get to this view controller from the main categories view controller, which then sets the SpendingCategory in the prepareForSegue method.

Here is as much as necessary from the TransactionViewController (I've omitted viewDidLoad since only labels and formats are set there):

#pragma mark Model initialization
- (void)setSpendingCategory:(SpendingCategory *)spendingCategory
{
    _spendingCategory = spendingCategory;
    [self setupFetchedResultsController];
    self.title = NSLocalizedString(@"Transactions", nil);
}

- (SpendingCategory *)spendingCategory
{
    return _spendingCategory;
}

- (void)setupFetchedResultsController
{
    NSDate *startDate = [NSDate startDateForCostPeriod:[self.spendingCategory getBiggestCostPeriod]];
    
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Expenditures"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"date" ascending:NO]];
    request.predicate = [NSPredicate predicateWithFormat:@"forSpendingCategory = %@ AND date >=%@", self.spendingCategory, startDate];
    
    self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
                                                                       managedObjectContext:self.spendingCategory.managedObjectContext
                                                                        sectionNameKeyPath:nil                                                                                      cacheName:nil];
}

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        [self updateBudgetAndBudgetLeft];
        [self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:NO];
    }
    
    - (void)updateBudgetAndBudgetLeft
    {
        self.budgetLabel.text = [NSString stringWithFormat:@"%@/%@",[self.spendingCategory convertCostToBiggestCostPeriodAsString], [self.spendingCategory getBiggestCostPeriodAsString]];
        
        double moneySpent = [[self.spendingCategory getExpendituresAmountForCostPeriod:[self.spendingCategory getBiggestCostPeriod]] doubleValue];
        double budget = [[self.spendingCategory convertCostToBiggestCostPeriod] doubleValue];
        //Money spent will be ADDED because negative values are already stored as negative
        double budgetLeftCalc = budget + moneySpent;
        
        if(budgetLeftCalc >= 0){
            NSNumber *budgetLeft = [NSNumber numberWithDouble:budgetLeftCalc];
            self.budgetLeftLabel.text = [NSString stringWithFormat:@"%@ %@", [budgetLeft getLocalizedCurrencyStringWithDigits:0], NSLocalizedString(@"left", nil)];
        } else {
            NSNumber *budgetLeft = [NSNumber numberWithDouble:-budgetLeftCalc];
            self.budgetLeftLabel.text = [NSString stringWithFormat:@"%@ %@", [budgetLeft getLocalizedCurrencyStringWithDigits:0], NSLocalizedString(@"over", nil)];
            self.budgetLeftLabel.textColor = [UIColor redColor];
        }
        
        if (moneySpent < 0) {
            self.progressBar.progress = -moneySpent/budget;
        } else {
            self.progressBar.progress = 0;
        }
    
    }

And the function getting the cost per period from the database is: (and this one obviously doesn't get in time the correct results):

- (NSNumber *)getExpendituresAmountForCostPeriod:(CostPeriod)costPeriod
{
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Expenditures"];
    [fetchRequest setResultType:NSDictionaryResultType];
    
    NSDate *startDate = [NSDate startDateForCostPeriod:[self getBiggestCostPeriod]];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"forSpendingCategory = %@ AND date >= %@", self, startDate];;
    
    
    //Define what we want
    NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: @"amount"];
    NSExpression *sumExpression = [NSExpression expressionForFunction: @"sum:"
                                                            arguments: [NSArray arrayWithObject:keyPathExpression]];
    
    //Defining the result type (name etc.)
    NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
    [expressionDescription setName: @"totalExpenditures"];
    [expressionDescription setExpression: sumExpression];
    [expressionDescription setExpressionResultType: NSDoubleAttributeType];
    
    // Set the request's properties to fetch just the property represented by the expressions.
    [fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
    
    
    // Execute the fetch.
    NSError *error = nil;
    NSArray *objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
    if (objects == nil) {
        return [NSNumber numberWithDouble:0];
    } else {
        if ([objects count] > 0) {
            return [[objects objectAtIndex:0] valueForKey:@"totalExpenditures"];
        } else {
            return [NSNumber numberWithDouble:0];
        }
    }
}

If you click on a transaction you get to the ExpenditureViewController which has a model of a Expenditure object *expenditure. This model has three attributes: date, amount and description.

If you change now some transactions or insert new transactions, this updateBudgetAndBudgetLeft doesn't do anything! The strange thing is, it does though after a while. If you wait 20 seconds and reload my TransactionViewController the changes occurred. The transactions however which are shown in my TransactionViewController are perfectly accurate immediately! Why? Why is it delayed? And why do I even need to do a seperate function updateBudgetAndBudgetLeft since I thought my NSFetchedResultsController does it automatically?! What do I miss in this whole core data jungle?

EDIT 2:

Complete CoredDataViewController (tableView is a property since I use embedded table views)

pragma mark - Fetching

- (void)performFetch
{
    if (self.fetchedResultsController) {
        if (self.fetchedResultsController.fetchRequest.predicate) {
            if (self.debug) NSLog(@"[%@ %@] fetching %@ with predicate: %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName, self.fetchedResultsController.fetchRequest.predicate);
        } else {
            if (self.debug) NSLog(@"[%@ %@] fetching all %@ (i.e., no predicate)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName);
        }
        NSError *error;
        [self.fetchedResultsController performFetch:&error];
        if (error) NSLog(@"[%@ %@] %@ (%@)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [error localizedDescription], [error localizedFailureReason]);
    } else {
        if (self.debug) NSLog(@"[%@ %@] no NSFetchedResultsController (yet?)", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
    }
    [self.tableView reloadData];
}

- (void)setFetchedResultsController:(NSFetchedResultsController *)newfrc
{
    NSFetchedResultsController *oldfrc = _fetchedResultsController;
    if (newfrc != oldfrc) {
        _fetchedResultsController = newfrc;
        newfrc.delegate = self;
        if ((!self.title || [self.title isEqualToString:oldfrc.fetchRequest.entity.name]) && (!self.navigationController || !self.navigationItem.title)) {
            self.title = newfrc.fetchRequest.entity.name;
        }
        if (newfrc) {
            if (self.debug) NSLog(@"[%@ %@] %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), oldfrc ? @"updated" : @"set");
            [self performFetch];
        } else {
            if (self.debug) NSLog(@"[%@ %@] reset to nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
            [self.tableView reloadData];
        }
    }
}

#pragma mark - UITableViewDataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return [[[self.fetchedResultsController sections] objectAtIndex:section] name];
}

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
    return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
}

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
    return [self.fetchedResultsController sectionIndexTitles];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return nil;
}

#pragma mark - NSFetchedResultsControllerDelegate

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    if(!self.reordering){
        if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext) {
            [self.tableView beginUpdates];
            self.beganUpdates = YES;
        }
    }
}

- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type
{
    if(!self.reordering){
        if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
        {
            switch(type)
            {
                case NSFetchedResultsChangeInsert:
                    [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                    
                case NSFetchedResultsChangeDelete:
                    [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
                    break;
            }
        }
    }
}


- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    if(!self.reordering){
        if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
        {
            switch(type)
            {
                case NSFetchedResultsChangeInsert:
                    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                    
                case NSFetchedResultsChangeDelete:
                    [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                    
                case NSFetchedResultsChangeUpdate:
                    [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                    
                case NSFetchedResultsChangeMove:
                    [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
            }
        }
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if(!self.reordering){
        if (self.beganUpdates) [self.tableView endUpdates];
    }
}

- (void)endSuspensionOfUpdatesDueToContextChanges
{
    _suspendAutomaticTrackingOfChangesInManagedObjectContext = NO;
}

- (void)setSuspendAutomaticTrackingOfChangesInManagedObjectContext:(BOOL)suspend
{
    if (suspend) {
        _suspendAutomaticTrackingOfChangesInManagedObjectContext = YES;
    } else {
        [self performSelector:@selector(endSuspensionOfUpdatesDueToContextChanges) withObject:0 afterDelay:0];
    }
}

-(void)stopSuspendAutomaticTrackingOfChangesInManagedObjectContextWithDelay:(double)delay
{
    //the delay is necessary in my app after row editing
    [self performSelector:@selector(endSuspensionOfUpdatesDueToContextChanges) withObject:0 afterDelay:delay];
}
Community
  • 1
  • 1
MichiZH
  • 5,587
  • 12
  • 41
  • 81
  • where are you calling `updateBudgetAndBudgetLeft`? have you implemented any of the FRC delegate methods? in general, a FRC only track the objects matching its fetch request predicate and entity and not properties of related objects. also, if a FRC is setup with a fetch request that is of `NSDictionaryResultType`, no tracking of objects will take place and you will have to keep track of changes on your own – Dan Shelly Dec 29 '13 at 18:32
  • updateBudgetAndBudgetLeft I'm calling frm within viewWillAppear. Yes I have the setup the complete FetchedResultsController (see my edit). How would you track some changes as it is here? I'm very confused and have really no idea where to continue with this error... – MichiZH Dec 30 '13 at 12:47
  • It is very unclear where you experience your problem (you gave code from 2 different view controllers, which one is problematic? remove irrelevant code). – Dan Shelly Dec 30 '13 at 14:54

1 Answers1

1

I'll try to give some hints but here it's quite difficult to follow the logic since there is too much code.

First, you told that updateBudgetAndBudgetLeft doesn't do anything, but I cannot see that this method is called anywhere. If viewWillAppear is the only place where that method is called it cannot be sufficient to recalculate data. In fact, you could change stuff but the controller should not disappear from screen…This is just my supposition.

Second, in

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if(!self.reordering){
        if (self.beganUpdates) [self.tableView endUpdates];
    }
}

you missed to check self.suspendAutomaticTrackingOfChangesInManagedObjectContext bool value.

Third, to isolate the problem I would suggest to leave out the CoreDataViewController. This class, I think it's used as a base class, can introduce problems that can be difficult to debug. Especially for people new to Core Data. So, for a moment forget it.

Based on these assumptions, the thing I would say is that the updateBudgetAndBudgetLeft should be called explicitly. When you use a NSFetchedResultsController, it will track the entity you set with (a interesting explanation of this can be found in NSFetchedResultsController pitfall). I guess Expenditures in this case but no one will call the update… method. My suggestion, for example, is to call that method in - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller. There you are sure that changes has been finished to be tracked.

Edit 1

you missed to check self.suspendAutomaticTrackingOfChangesInManagedObjectContext bool value. What do you mean here?

If you have suspend the tracking of changes, you should not call the endUpdates method. If I remember well, also the Stanford code does a similar thing. Check it to be sure.

So you're suggestion is to overwrite this controllerDidChangeContent in every view controller (which is subclassing CoreDataViewController) and call updateBudgetAndBudgetLeft there?

Yes. It could be an option. Also calling super should work but in this case it depends if you want to update even if changes do not to tracked. Anyway, I would work without that base class.

Why isn't it sufficient to call this method in viewWillAppear? I mean it should be called each time the view gets visible or not?

It depends when you recalculate data. If you perform it and the controller stays visible, the update… won't be called.

Edit 2

As per the documentation and based on this newer question iOS FetchRequest on aggregate functions: How to include pending changes?, if the update… method is used with type NSDictionaryResultType, it's necessary to perform a save first. After that it's possible to call it as normally. In other words, every time you modify an Expenditures item, you should do a save followed by an update…. Obviously, saving too much will hit the disk and so could cause the application to goes slow but here you need to measure. Otherwise you can perform the aggregation at memory level. To do it, just filter the correct entities to do it. This can be done easily at controllerDidChangeContent or in viewWillAppear.

Community
  • 1
  • 1
Lorenzo B
  • 33,216
  • 24
  • 116
  • 190
  • I think I'm starting to understand...first question: you missed to check self.suspendAutomaticTrackingOfChangesInManagedObjectContext bool value. What do you mean here? Where should that go? And second question: So you're suggestion is to overwrite this controllerDidChangeContent in every view controller (which is subclassing CoreDataViewController) and call updateBudgetAndBudgetLeft there? And by the way: Why isn't it sufficient to call this method in viewWillAppear? I mean it should be called each time the view gets visible or not? – MichiZH Dec 30 '13 at 15:10
  • OK again to 1: I'll check it. To Nr. 2: Without which base class would you work? CoreDataViewController? Why? I need to implement all these delegate methods don't I? And Nr. 3: But am I missing something..every time I segue back or segue to a view controller the viewWillAppear will trigger. So if I recalculate anything there and set the labels again it should work shouldn't it? But currently I could cry...I've switched to the app delegate initialization of core data as the template of apple does and now nothing gets saved anymore and crashes. I really doubt I'll ever finish this app :( – MichiZH Dec 30 '13 at 15:34
  • @MichiZH 2) Yes, implement delegates in each controllers. This is to isolate the problem. My suggestion is not to use third party code to speed up development since problems can arise everywhere. 3) Once you finished to recalculate the values, you could just reassign them to the UI components. FINALLY. What crash you have? – Lorenzo B Dec 30 '13 at 15:38
  • 2) But it makes sense to have a file with the delegate methods which I'm subclassing doesn't it? since most methods are the same anyway? 3) I thought that my code is worked through step by step by the compiler unless I multithread. So as I've coded it the recalculated value should be automatically assigned to the labels? Yeah first it just didn't save any values when I reopened the app and now I cannot even open my app in the sim anymore. So I've time machine'd back and I'm using again the UIManagedDocument version from the Stanford course which works.. – MichiZH Dec 30 '13 at 15:43
  • @MichiZH Yes it makes sense but Core Data it's quite difficult to diagnose so, for this, I suggested to rely on separated stuff but if you are ok with your code leave as is. Hope you'll find a solution. P.S. If you found my answer useful you could just upvote. Cheers. – Lorenzo B Dec 30 '13 at 15:47
  • @MichiZH Based on your question I'll expanded a little bit my answer. – Lorenzo B Dec 30 '13 at 17:23