9

Is it possible to receive a callback or notification in the parent Entity when any one it's relationship objects changes? This works great when an attribute of the Entity changes. The following method...

- (void)didChangeValueForKey:(NSString *)key

is invoked on my Entity subclass. However this method is not invoked when an attribute in one of the relationships changes.

What I'm trying to do is update the timeStamp attribute on my parent Entity when any one of its attributes or relationship objects changes.

Sean S Lee
  • 1,274
  • 9
  • 24
Nate Potter
  • 3,222
  • 2
  • 22
  • 24

2 Answers2

6

The parent entity can set itself as an observer of the relationship and it will get notified when that relationship changes. However that will only be fired when the actual relationship (adding or removing a child) occurs.

To watch for a specific child entity is far more tricky. There are a couple of ways to go about it:

  1. Have the child ping the parent when its properties change.
  2. Have the parent listen for NSManagedObjectContextDidSaveNotification and look to see if any of its children are in that save
  3. Have the parent observe the values on the children.

There may be other solutions but of the three I recommend #2. It is pretty easy to set up and the performance impact is pretty minimal.

Marcus S. Zarra
  • 46,571
  • 9
  • 101
  • 182
  • I love that I can send a question out into the void and receive a response! I decided on solution #2. I'm looping through the parents relationships and checking if any of the relationship managedObjects are the same as the notification managedObject. – Nate Potter Oct 13 '11 at 03:09
  • Remember that you can run a predicate against those `NSSet` instances that come back. No need to loop :) The predicate could be something like `@"entity.name == %@ && parent == %@"` which would give you back any children of the current entity that are of the relationship you care about. – Marcus S. Zarra Oct 13 '11 at 17:16
  • Awesome, thanks! Works like a charm. I hope you don't mind if I include a link to your blog post on this subject ["Parent Watching it's Child"](http://www.cimgf.com/2011/10/14/parent-watching-its-child/) and also a plug for your book ["Core Data"](http://www.amazon.com/Core-Data-Apples-API-Persisting/dp/1934356328/ref=sr_1_3?ie=UTF8&qid=1319500666&sr=8-3) I have referenced it frequently. Also, in my case the other thing I needed to do was create Primitive Accessors for the timestamp in the NSManagedObject subclass, to prevent it from going into an infinite notification loop. – Nate Potter Oct 24 '11 at 23:55
  • No problem at all, glad it helped! – Marcus S. Zarra Oct 26 '11 at 00:17
  • @MarcusS.Zarra Where would the child ping the parent - within a setter, after the willChangeValueForKey:, setPrimitiveValue:forKey:, didChangeValueForKey: sequence? – SAHM Mar 30 '16 at 20:03
  • 1
    @SAHM Options 2 and 3 are superior. For Option 1 you can do it in an overridden setter (or didSet from Swift) – Marcus S. Zarra Mar 31 '16 at 00:05
  • Thanks Marcus. I do like option #2 and I did attempt it, but I found it difficult to figure out which attributes had changed on the changed objects, and I am only interested in certain attribute changes. I also tried #3 but since I had a case where the parent had to observe two relationships deep, I found that things got very confusing and complicated. I don't like overriding the setter, but I am having a very hard time finding good examples of this more advanced KVO for Core Data (and I am still using Objective C unfortunately). – SAHM Mar 31 '16 at 01:51
  • Additionally, KVO seems to fire for an object attribute regardless of whether the attribute was set or not when refreshObject:mergeChanges: is called, which is often for me. This combined with the above seemed to make option #1 the most straightforward for me, but I would love to read more on the subject if you can point me to more advanced resources that would address some of these issues. Sorry to keep asking questions, I don't know anyone else that programs in real life so SO really is a lifeline for me. – SAHM Mar 31 '16 at 03:07
  • Sounds like you have an overly-complicated data model and you are going at things the hard way. I would suggest posting your own question, with an image of your data model and ask that question. Then send me a link to that question and I will be happy to take a look at it. My email address is not hard to track down :) – Marcus S. Zarra Mar 31 '16 at 15:19
2

In the other answer I found 1,2 and 3 too inefficient. Particularly 2 and the example in the "Parent Watching it's Child" blog post. My issue with that is every single parent had to respond to the context notification and essentially every object being saved if it is the child (never mind the fact ContextDidSave is more appropriate in that case!). Instead, I would propose an option 4:

  1. Have the child override didSave and broadcast a NSManagedObjectContextDidSaveNotification containing the parent.

My solution is more efficient and feels more object-oriented to me since the object that is changing is responding to its own change. To implement this, within the child object use:

-(void)didSave{
    [super didSave];
    // notify that the parent has changed.
    [[NSNotificationCenter defaultCenter] postNotificationName:NSManagedObjectContextObjectsDidChangeNotification
                                                        object:self.managedObjectContext
                                                      userInfo:@{NSUpdatedObjectsKey : [NSSet setWithObject:self.parent]}];;
}

To update the parent timestamp, the following neat solution (second last post) I've been using might help, e.g. in the parent use:

- (void) awakeFromInsert
{
    [super awakeFromInsert];
    // set the default dates
    NSDate* date = [NSDate date];
    self.timestamp = date;
    //allow any future modifications to change the timestamp
    _finishedStartup = YES;
}

- (void) awakeFromFetch
{
    [super awakeFromFetch];
    // we should already have creation and modified dates.
    _finishedStartup = YES;
}

- (void) didChangeValueForKey: (NSString *) thisKey
{
    [super didChangeValueForKey: thisKey];

    if(![thisKey isEqualToString:@"timestamp"] && // prevent infinite loop
       ![self isFault] &&
       ![[[self managedObjectContext] undoManager] isUndoing] &&
       _finishedStartup) // ensures we arent being called by the object being loaded from fetched data.
    {
        self.timestamp = [NSDate date];
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133