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

12,807

Solution 1

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.

Solution 2

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.

Share:
12,807

Related videos on Youtube

nmdias
Author by

nmdias

Updated on February 17, 2020

Comments

  • nmdias
    nmdias about 4 years

    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

    • Leonardo
      Leonardo over 11 years
      After setting to NO did you try to save the context ? I suppose yes. Did you also tried to see if _fetchedResultsController has still the controller as delegate ? Did you put a breakpoint to see if didChangeObject:atIndexPath: is entered ? Also, I am not sure of this, but I suppose the predicate should not be built with a string, but rather directly with [NSPredicate predicateWithFormat:@"isOpen==%@",[NSNumber numberWithBool:NO]]
    • nmdias
      nmdias over 11 years
      I did. It does save the context has it should without generating any errors whatsoever. Yes, it still does. Every change to -> other <- managedObects, reflects on the tableView being updated. didChangeObject:atIndexPath: does not get called for this particular case! I don't understand why. I have 2 more methods, with the EXACT same code, they just happen to be a different entity. And they work perfectly. Also, there shouldn't be an issue with that predicate. All other methods happen to have the same predicate @"isOpen == YES". And they do work flawlessly. Thank you @Leonardo
    • Jody Hagins
      Jody Hagins over 11 years
      It is because the way you are doing your filter, through a relationship. The data in the relationship changes, but the FRC already has fetched those objects. For confirmation, please post the code for one of the other methods that change the database but "works right." Also, you said you tried to reload and refetch, but it did not work. That's odd, so please post what you did to reload/refetch the data.
    • nmdias
      nmdias over 11 years
      Just posted Update 3. I spent all day with this ... And then you came along! It works now ... I'm stumped! Thank you so much Sir! reloadData after calling performFetch. "if (![[self fetchedResultsController] performFetch:&error])" does work now! But I do have to say, I am really looking to do this without invoking reloadData. reloadData takes out my animations. You think there's anyway I can accomplish this without it?
    • nmdias
      nmdias over 11 years
      @JodyHagins you also said, "The data in the relationship changes, but the FRC already has fetched those objects". I'm tying to understand this. It did fetched the objects. But by changing that property to "NO", they should no longer appear. Because my predicate filters them out. p.s. ClockSet has many ClockPairs. If clockSet.isOpen is set to NO. Then all clockPairs should disappear from my tableView. I shouldn't need to force a manual performFetch, to refetch, and then reloadData. Should I?
    • nmdias
      nmdias over 11 years
      Ok, resultsController only monitors objects belonging to the entity provided in NSEntityDescription. That's why it didn't work. I think this is what you were saying by "The data in the relationship changes, but the FRC already has fetched those objects" Thank you so much everyone for all the great help and pointers! I Really appreciate it!
    • Jody Hagins
      Jody Hagins over 11 years
      So, my assumptions were correct. I will try to post an explanation as to exactly what is happening, but I am stepping out at this moment, and do not have time. I'll try to follow up with the appropriate explanation later tonight, unless someone else beats me to it.
  • nmdias
    nmdias over 11 years
    No success there. That thought did cross my mind. And no error is generated. Oddly enough, going to background is not enough for it to be updated. It must be closed and then reopened. Only when reopening, tableView gets the correct results. Both going to background and terminating application call for saveContext. And that's pretty much all they do. Thank you for your suggestion @Alan. I really appreciate it.
  • nmdias
    nmdias almost 11 years
    I never got to thank you for this. Thank You sir. Half a year later and everything you said makes sense to me now, although it was somewhat confusing at the time. And yes, I also though it was a big ugly, but if I remember correctly that did fix the problem. Thank you!
  • Jacob
    Jacob over 10 years
    I want to kiss you on the face for this. I had to change my predicate so that it included the relationship which encapsulated all objects in the set plus the "property == %@", @YES predicate to further filter. Then when I flip property from NO to YES, my fetched results controller receives updates from the MOC-did-save notification.
  • lostintranslation
    lostintranslation over 9 years
    Was there ever a bug report filed? This looks to be a few years old, are there other solutions for forcing a FRC to 'see' changes made on a keyPath?
  • Jody Hagins
    Jody Hagins about 9 years
    If your issue is different, then create a new question. Be sure to include all necessary code to reproduce your problem.
  • Benjohn
    Benjohn about 9 years
    Heavens! This is one of those answers where you think "How the hell did you find this out, dude?!". You have my most serious respect, and also, thanks. Nice one.