Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing a managed object property doesn't trigger NSFetchedResultsController to update the table view

I have a fetchedResultsController with a predicate, where "isOpen == YES"

When calling for closeCurrentClockSet, I set that property to NO. Therefore, it should no longer appear on my tableView.

For Some Reason, this is not happening.

Can someone help me figure this out please?

-(void)closeCurrentClockSet
{

    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"isOpen == YES"];

    NSArray *fetchedObjects =
        [self fetchRequestForEntity:@"ClockSet"
                      withPredicate:predicate
             inManagedObjectContext:[myAppDelegate managedObjectContext]];

    ClockSet *currentClockSet = (ClockSet *)fetchedObjects.lastObject;

    [currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];

}

--

I have a couple of methods more, using the exact same approach, by calling a custom fetchRequestForEntity:withPredicate:inManagedObjectContext method.

In those methods, when changing a property, tableView get correctly updated! But this one above (closeCurrentClockSet), doesn't! I can't figure out why.

--

My implementation for my fetchedResultsController, is from Apple's documentation.

Also, another detail. If I send my App, to the background. Close it and re-open, tableView shows updated as it should!

I have tried my best to follow previous questions here on stackOverflow. No luck. I also NSLogged this to the bone. The object is getting correctly fetched. It is the right one. isOpen Property is being correctly updated to NO. But for some reason, my fetchedResultsController doesn't update tableView.

I did try a couple a "hammer" solutions, like reloadData and calling performFetch. But that didn't work. Or would make sense to used them...

EDIT: scratch that, it DID work, calling reloadData imediatly after performFetch on my resultsController but using reloadData is hammering a solution. Plus, it takes out all animations. I want my controller to auto-update my tableView.

Can someone help me figure this out?

Any help is greatly appreciated!

Thank you,

Nuno

EDIT:

The complete implementation.

fetchedResultsController is pretty standard and straightforward. Everything else is from Apple's documentation

- (NSFetchedResultsController *)fetchedResultsController
{

    if (_fetchedResultsController) {
        return _fetchedResultsController;
    }

    NSManagedObjectContext * managedObjectContext = [myAppDelegate managedObjectContext];

    NSEntityDescription *entity  =
        [NSEntityDescription entityForName:@"ClockPair"
                    inManagedObjectContext:managedObjectContext];

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        [fetchRequest setEntity:entity];

    NSString *predicate = [NSString stringWithFormat: @"clockSet.isOpen == YES"];
        [fetchRequest setPredicate: [NSPredicate predicateWithFormat:predicate]];

    NSSortDescriptor *sortDescriptor1 =
        [[NSSortDescriptor alloc] initWithKey:@"clockIn" ascending:NO];

    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];

        [fetchRequest setSortDescriptors:sortDescriptors];
        [fetchRequest setFetchBatchSize:20];

    NSFetchedResultsController *theFetchedResultsController =
        [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                            managedObjectContext:managedObjectContext
                                              sectionNameKeyPath:nil
                                                       cacheName:@"Root"];


    _fetchedResultsController = theFetchedResultsController;
    _fetchedResultsController.delegate = self;

    return _fetchedResultsController;

}

--

Boilerplate code from Apple's documentation:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    [self.tableView beginUpdates];
}



- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{

    UITableView *tableView = self.tableView;

    switch(type) {

        case NSFetchedResultsChangeInsert:

            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                             withRowAnimation:UITableViewRowAnimationTop];

            break;

        case NSFetchedResultsChangeDelete:

            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];

            break;

        case NSFetchedResultsChangeUpdate:

            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];

            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                             withRowAnimation:UITableViewRowAnimationFade];

            break;

        case NSFetchedResultsChangeMove:

            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationLeft];

            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                             withRowAnimation:UITableViewRowAnimationTop];

            break;
    }
}



- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id )sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type
{

    UITableView *tableView = self.tableView;

    switch(type) {

        case NSFetchedResultsChangeInsert:

            [tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                     withRowAnimation:UITableViewRowAnimationFade];

            break;

        case NSFetchedResultsChangeDelete:

            [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                     withRowAnimation:UITableViewRowAnimationFade];

            break;
    }
}



- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    [self.tableView endUpdates];
}

1ST UPDATE:

Tracking [managedObjectContext hasChanges] does return YES, as it should. But fetchedResultsController doesn't update the tableView

2ND UPDATE

didChangeObject:atIndexPath: does not get called for this particular case! I have 2 more methods, with the EXACT same code, they just happen to be a different entity. And they work perfectly. Thank you @Leonardo for pointing this out

3TH UPDATE this method, follows the same rules. But does actually work.

- (void)clockOut
{
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"isOpen == %@", [NSNumber numberWithBool:YES]];

    NSArray * fetchedObjects =
        [self fetchRequestForEntity:@"ClockPair"
                      withPredicate:predicate
             inManagedObjectContext:[myAppDelegate managedObjectContext]];

    ClockPair *aClockPair = (ClockPair *)fetchedObjects.lastObject;

    aClockPair.clockOut = [NSDate date];
    aClockPair.isOpen   = [NSNumber numberWithBool:NO];


}

Anyone has any other ideas for what I might be missing?

Thank you,

Nuno

like image 539
nmdias Avatar asked Sep 11 '12 13:09

nmdias


2 Answers

OK, I will explain your problem, then I will let you judge whether it is a bug in FRC or not. If you think it is a bug, then you really should file a bug report with apple.

Your fetch result controller predicate is like this:

NSString *predicate = [NSString stringWithFormat: @"clockSet.isOpen == YES"];

which is a valid predicate for a boolean value. It is going to follow the relationship of the clockSet entity and grab its isOpen attribute. If it is YES then those objects will be accepted into the array of objects.

I think we are good up to here.

Now, if you change one of clockSet.isOpen attributes to NO, then you expect to see that object disappear from your table view (i.e., it should no longer match the predicate so it should be removed from the array of fetched objects).

So, if you have this...

[currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];

then, whichever top-level object has a relationship to the currentClockSet should "disappear" from your FRC array of fetched results.

However, you do not see it disappear. The reason is that the object monitored by the FRC did not change. Yes, the predicate key path changed, but the FRC holds entities of ClockPair and a ClockSet entity actually changed.

You can watch the notifications fly around to see what's going on behind the scenes.

Anyway, the FRC will use a key path when you do a fetch, but it will not monitor changes to objects that are not in its actual set of fetched objects.

The easiest work-around is to "set" an attribute for the object that holds this key path object.

For example, I noticed that the ClockPair also has an isOpen attribute. If you have an inverse relationship, then you could do this...

currentClockSet.isOpen = NO;
currentClockSet.clockPair.isOpen = currentClockSet.clockPair.isOpen;

Notice that you did not actually change the value at all. However, the setter was called, which triggered KVO, and thus the private DidChange notification, which then told the FRC that the object changed. Thus, it re-evaluates the check to see if the object should be included, finds the keypath value changed, and does what you expect.

So, if you use a key path in your FRC predicate, if you change that value, you need to worm your way back to all the objects in the FRC array and "dirty them up" so that those objects are in the notification that is passed around about object changes. It's ugly, but probably better than saving or changing your fetch request and refetching.

I know you don't believe me, so go ahead and try it. Note, for it to work, you have to know which item(s) in the FRC array of objects would be affected by the change, and "poke" them to get the FRC to notice the change.

The other option, as I mentioned earlier, is to save the context, and refetch the values. If you don't want to save the context, you can make the fetch include updates in the current context, without refreshing from the store.

I have found that faking a change to an object that the FRC is watching is the best way to accomplish a re-evalution of predicates that are key paths to other entities.

OK, so, whether this is a bug or not is up for some debate. Personally, I think if the FRC is going to monitor a keypath, it should do it all the way, and not partially like we see here.

I hope that make sense, and I encourage you to file a bug report.

like image 129
Jody Hagins Avatar answered Oct 22 '22 11:10

Jody Hagins


You ran into a similar problem.

I know this question is pretty old but I hope this helps someone else:

The easiest way was to introduce a new property named lastUpdated: NSDate in the parent object.

I had a Conversation which contains several Messages. Whenever the isRead flag of the message was updated, I needed an update in the ConversationOverviewViewController that only displays Conversations. Furthermore, the NSFetchedResultsController in the ConversationOverviewVC only fetches Conversations and doesn't know anything about a Message.

Whenever a message was updated, I called message.parentConversation.lastUpdated = NSDate(). It's an easy and useful way to trigger the update manually.

Hope this helps.

like image 6
Chris Avatar answered Oct 22 '22 11:10

Chris