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

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.