58

I have a NSManagedObjectContext in which I have a number of subclasses of NSManagedObjects such that some are containers for others. What I'd like to do is watch a top-level object to be notified of any changes to any of its properties, associations, or the properties/associations of any of the objects it contains.

Using the context's 'hasChanges' doesn't give me enough granularity. The objects 'isUpdated' method only applies to the given object (and not anything in its associations). Is there a convenient (perhaps, KVO-based) was I can observe changes in a context that are limited to a subgraph?

Dan Beaulieu
  • 19,406
  • 19
  • 101
  • 135
David Carney
  • 2,200
  • 1
  • 19
  • 28

3 Answers3

129

You will want to listen for the NSManagedObjectContextObjectsDidChangeNotification to pick up all changes to your data model. This can be done using code like the following:

[[NSNotificationCenter defaultCenter] 
      addObserver:self 
         selector:@selector(handleDataModelChange:) 
             name:NSManagedObjectContextObjectsDidChangeNotification 
           object:myManagedObjectContext];

which will trigger -handleDataModelChange: on any changes to the myManagedObjectContext context.

Your -handleModelDataChange: method would look something like this:

- (void)handleDataModelChange:(NSNotification *)note
{
    NSSet *updatedObjects = [[note userInfo] objectForKey:NSUpdatedObjectsKey];
    NSSet *deletedObjects = [[note userInfo] objectForKey:NSDeletedObjectsKey];
    NSSet *insertedObjects = [[note userInfo] objectForKey:NSInsertedObjectsKey];

    // Do something in response to this
}

As you can see, the notification contains information on which managed objects were updated, deleted, and inserted. From that information, you should be able to act in response to your data model changes.

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • Thanks! I wasn't aware of that notification. It still seems like massive overkill to have to perform a search each time the notification is sent. That said, I'm not sure how it could be optimized even by Apple; fundamentally, it's still a graph traversal. – David Carney Mar 19 '10 at 16:23
  • 4
    If you use an NSPredicate to filter through these objects it is actually quite performant because everything is in memory. I use this solution in several applications for the iPhone and there has yet to be a performance bottleneck in this area. – Marcus S. Zarra Mar 24 '10 at 14:34
  • Ah, good call on using an NSPredicate. Hadn't thought of that. – David Carney Mar 24 '10 at 18:42
  • 4
    You can also use notification `NSManagedObjectContextDidSaveNotification` to filter the changes after each save to the context. – krasnyk May 04 '10 at 08:57
  • 5
    Careful: The objects in the NSNotification's userInfo dictionary are actually of type NSSet and NOT NSArray! – rluba Oct 31 '10 at 21:55
  • @racha - Good catch. I'd somehow screwed that up when copying code over. I've corrected the sample code in the answer. – Brad Larson Oct 31 '10 at 22:05
  • 6
    You didn't mention this, but I want to point out for clarity that you can not issue a save to the object using this notification. It is posted during processPendingChanges, after the changes have been processed, but before it is safe to call save:. – memmons Nov 04 '10 at 14:30
  • @Harkonian - Yes, that's a good point. Something like this should be used by a controller to update the UI in response to changes in the underlying data model. You can use it to make some changes to the model, but trying to save in here somewhere would probably be a bad idea. – Brad Larson Nov 04 '10 at 15:37
  • @BradLarson I simply use NSTimer to delay save by a few seconds. Also allows me to cancel save in case a new change comes along, and retry saving, so the saves are somewhat grouped. – Ivan Vučica Oct 04 '11 at 10:26
  • You could also `dispatch_async` the save to the main queue to delay the save. – hypercrypt Oct 13 '11 at 08:33
  • 1
    If you are using MagicalRecord // Listen to changes in ROOT SavingContext [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDataModelChange:) name:NSManagedObjectContextObjectsDidChangeNotification object:[NSManagedObjectContext MR_rootSavingContext]]; – Mohamed Saleh Dec 12 '15 at 13:40
19

here's a simple example in Swift:

    NotificationCenter.default.addObserver(forName: .NSManagedObjectContextObjectsDidChange, object: nil, queue: nil) { note in
        if let updated = note.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updated.count > 0 {
            print("updated: \(updated)")
        }

        if let deleted = note.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>, deleted.count > 0 {
            print("deleted: \(deleted)")
        }

        if let inserted = note.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserted.count > 0 {
            print("inserted: \(inserted)")
        }
    }
Dan Beaulieu
  • 19,406
  • 19
  • 101
  • 135
Ilias Karim
  • 4,798
  • 3
  • 38
  • 60
  • 3
    Typically one needs to keep track of the return value `let observation = NSNotificationCenter.defaultCenter().addObserverForName(..){..}` to be able to stop observing by calling `NSNotificationCenter.defaultCenter().removeObserver(observation)`. – Lev Landau Sep 06 '16 at 09:33
  • 1
    @DanBeaulieu, if you have used the blocked based version , `NSNotificationCenter.defaultCenter().addObserverForName(..){‌​..}`, you need to keep hold of the returned observation. – Lev Landau Jan 29 '17 at 10:43
  • @LevLandau I'll undo my edit and read up, thanks for informing – Dan Beaulieu Jan 29 '17 at 16:55
0

for me it's just lost following two func, maybe this save hours for someone

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    tableView.beginUpdates()
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    tableView.endUpdates()
}
zmj110
  • 67
  • 4