Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to listen for specific entity saves/updates in CoreData

First thing I tried was to use a FetchedResultsController to solve my problem. This was an atypical use of FRC in that I was not updating a TableView, I was using it just to determine if entities were changing so I knew when to save to the server:

self.fetchedResultsController = [Document MR_fetchAllSortedBy:@"timestamp"
                                                    ascending:NO
                                                withPredicate:[NSPredicate predicateWithFormat:@"report.remoteId != nil && dirty == YES"]
                                                      groupBy:nil
                                                     delegate:self
                                                    inContext:_managedObjectContext];

The issue here is that FRC does not receive updates when a relationship entity changes. IE if a report.remoteId goes from nil to non-nil I will not see the update as the FRC listens for changes on only the Document entity. Limitation outlined here: http://www.mlsite.net/blog/?p=825 and here Changing a managed object property doesn't trigger NSFetchedResultsController to update the table view

Not sure I really want to implement the workaround as I fell like I am using the FRC for things it was not meant to do. I don't think apple is going to fix this limitation, so I don't really want to smash a round peg into a square hole to solve my problem.

A couple of options

Fire a notification in code after I have saved entities, then listen for that elsewhere. Only thing I don't like about this is that it up to the programmer to do this and keep it up to date, ie if someone comes along and saves an entity in another place they must remember to fire the notification.

OR

Listen for saves to the default MOC. This is what I would really like to do. Something like this:

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

- (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
}

Here is my dilemma with this. This will listen to all changes on the default MOC. I really only care about changes to a couple of entities. So yeah I can filter out the entities I care about in each call. BUT, I have a case in which I save a ton of other entities in which I do not care about. This means that the NSManagedObjectContextObjectsDidChangeNotification will fire a ton and most of the time I will not care. I don't want to slow down my app by constantly receiving this notification and taking time to filter out all entities that I do not care about.

Is there a way to listen for specific entity saves? Either in CoreData, or MagicalRecord?

If the answer is no, is there a good alternative for listening to changes to a specific entity (and its relationship)?

like image 447
lostintranslation Avatar asked Dec 30 '14 15:12

lostintranslation


2 Answers

There's no way to listen to changes to a specific set of entities; catching NSManagedObjectContextObjectsDidChangeNotification (or did or will save) and filtering is the correct approach, with the caveat that key-value observing is also an option if you're talking about specific instances of entities.

However, worth observing is that NSManagedObjectID is thread-safe and provides a getter for the NSEntityDescription. So you could e.g.

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

    NSMutableArray *objectIDs = [NSMutableArray array];
    [objectIDs addObjectsFromArray:[updatedObjects.allObjects valueForKey:@"objectID"]];
    [objectIDs addObjectsFromArray:[deletedObjects.allObjects valueForKey:@"objectID"]];
    [objectIDs addObjectsFromArray:[insertedObjects.allObjects valueForKey:@"objectID"]];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(){
        NSSet *affectedEntities = [NSSet setWithArray:[objectIDs valueForKeyPath:@"entity.name"]];

        if([affectedEntities containsObject:@"InterestingEntity"]) {
            // get back onto the main thread and do some proper work;
            // possibly having collated the relevant object IDs here
            // first — whatever is appropriate
        }
    });
}
like image 104
Tommy Avatar answered Nov 09 '22 02:11

Tommy


After reading through the docs and some other forms I also came up with another possible solution.

Why not override the -(void) didSave method of the managed object to catch a change on that object.

@implementation Report (helper)

- (void) didSave {
    [super didSave];

    // if not on default context no-op
    if ([NSManagedObjectContext MR_defaultContext] != self.managedObjectContext) return;

    // send custom notification here  
}

@end

Still deals with sending a custom notification, but at least in encapsulated in the managed object, such that devs don't have to worry about where to send the notification.

On the plus side this only executes for the managed object that I care about.

Minus is that it is called for all managed object contexts. However I don't think the MOC check is going to be a heavy hitter.

Not sure if this is better/worse than listening on the default MOC for saves and filtering. Plus in that case is that I know I am listening for saves only on that MOC. Even though I can filter in a background task I am still filtering tons of data in that case that I don't need to deal with.

Thoughts?

like image 40
lostintranslation Avatar answered Nov 09 '22 01:11

lostintranslation