How do I share a Core Data store between processes using NSDistributedNotifications?
Asked Answered
S

7

8

Background

I've already posted a question about the basics of sharing a Core Data store between processes.

I'm trying to implement the recommendations given and I'm running into problems.

My Goal

I have two processes - the Helper App and the UI. They both share a single data store. I want the UI to update it's NSManagedObjectContext when the Helper App has saved new data to the store.

Current Program Flow

  1. The Helper App Process writes data to the Store.

  2. In the Helper App, I listen for NSManagedObjectContextDidSaveNotification notifications.

  3. When the context is saved, I encode the inserted, deleted and updated objects using their URI representations and NSArchiver.

  4. I send an NSNotification to the NSDistributedNotificationCenter with this encoded dictionary as the userInfo.

  5. The UI Process is listening for the save notification. When it receives the notification, it unarchives the userInfo using NSUnarchiver.

  6. It looks up all the updated/inserted/deleted objects from the URIs given and replaces them with NSManagedObjects.

  7. It constructs an NSNotification with the updated/inserted/deleted objects.

  8. I call mergeChangesFromContextDidSaveNotification: on the Managed Object Context of the UI Process, passing in the NSNotification I constructed in the previous step.

The Problem

Inserted objects are faulted into the UI Managed Object Context fine and they appear in the UI. The problem comes with updated objects. They just don't update.

What I've tried

  1. The most obvious thing to try would be to pass the save Notification from the Helper App process to the UI process. Easy, right? Well, no. Distributed Notifications won't allow me to do that as the userInfo dictionary is not in the right format. That's why I'm doing all the NSArchiving stuff.

  2. I've tried calling refreshObject:mergeChanges:YES on the NSManagedObjects to be updated, but this doesn't seem to have any effect.

  3. I've tried performing the mergeChangesFromContextDidSaveNotification: selector on the main thread and the current thread. Neither seems to affect the result.

  4. I've tried using mergeChangesFromContextDidSaveNotification: before between threads, which of course is much simpler and it worked perfectly. But I need this same functionality between processes.

Alternatives?

Am I missing something here? I'm consistently getting the feeling I'm making this much more complex than it needs to be, but after reading the documentation several times and spending a few solid days on this, I can't see any other way of refreshing the MOC of the UI.

Is there a more elegant way of doing this? Or am I just making a silly mistake somewhere in my code?

The Code

I've tried to make it as readable as possible, but it's still a mess. Sorry.

Helper App Code

   -(void)workerThreadObjectContextDidSave:(NSNotification *)saveNotification {
        NSMutableDictionary *savedObjectsEncodedURIs = [NSMutableDictionary dictionary];
        NSArray *savedObjectKeys = [[saveNotification userInfo] allKeys];

        for(NSString *thisSavedObjectKey in savedObjectKeys) {
            // This is the set of updated/inserted/deleted NSManagedObjects.
            NSSet *thisSavedObjectSet = [[saveNotification userInfo] objectForKey:thisSavedObjectKey];
            NSMutableSet *thisSavedObjectSetEncoded = [NSMutableSet set];

            for(id thisSavedObject in [thisSavedObjectSet allObjects]) {
                // Construct a set of URIs that will be encoded as NSData
                NSURL *thisSavedObjectURI = [[(NSManagedObject *)thisSavedObject objectID] URIRepresentation];
                [thisSavedObjectSetEncoded addObject:thisSavedObjectURI];
            }
            // Archive the set of URIs.
            [savedObjectsEncodedURIs setObject:[NSArchiver archivedDataWithRootObject:thisSavedObjectSetEncoded] forKey:thisSavedObjectKey];
        }

        if ([[savedObjectsEncodedURIs allValues] count] > 0) {
            // Tell UI process there are new objects that need merging into it's MOC
            [[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.synapticmishap.lapsus.save" object:@"HelperApp" userInfo:(NSDictionary *)savedObjectsEncodedURIs];
        }
    }

UI Code

-(void)mergeSavesIntoMOC:(NSNotification *)notification {
    NSDictionary        *objectsToRefresh        = [notification userInfo];
    NSMutableDictionary *notificationUserInfo    = [NSMutableDictionary dictionary];
    NSArray *savedObjectKeys = [[notification userInfo] allKeys];

    for(NSString *thisSavedObjectKey in savedObjectKeys) {
        // Iterate through all the URIs in the decoded set. For each URI, get the NSManagedObject and add it to a set.
        NSSet *thisSavedObjectSetDecoded = [NSUnarchiver unarchiveObjectWithData:[[notification userInfo] objectForKey:thisSavedObjectKey]];
        NSMutableSet *savedManagedObjectSet = [NSMutableSet set];

        for(NSURL *thisSavedObjectURI in thisSavedObjectSetDecoded) {
            NSManagedObject *thisSavedManagedObject = [managedObjectContext objectWithID:[persistentStoreCoordinator managedObjectIDForURIRepresentation:thisSavedObjectURI]];
            [savedManagedObjectSet addObject:thisSavedManagedObject];
            // If the object is to be updated, refresh the object and merge in changes.
            // This doesn't work!
            if ([thisSavedObjectKey isEqualToString:NSUpdatedObjectsKey]) {
                [managedObjectContext refreshObject:thisSavedManagedObject mergeChanges:YES];
                [managedObjectContext save:nil];
            }
        }
        [notificationUserInfo setObject:savedManagedObjectSet forKey:thisSavedObjectKey];
    }
    // Build a notification suitable for merging changes into MOC.
    NSNotification *saveNotification = [NSNotification notificationWithName:@"" object:nil userInfo:(NSDictionary *)notificationUserInfo];
    [managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
                                    withObject:saveNotification
                                 waitUntilDone:YES];
}
Stowage answered 29/11, 2009 at 13:17 Comment(4)
Is there a reason why you are trying to use inter-process communication rather than watching the persistent store file itself to see when it is updated?Congratulant
Not really. But if I watched the store file, I'd need to have some way of refreshing the Managed Object Context and the only way I can find of doing this is by the mergeChangesFromContextDidSaveNotification: method. I suspect I'm missing a really obvious way of refreshing the MOC without relying on this method.Stowage
for(id thisSavedObject in [thisSavedObjectSet allObjects]) could be written as for(id thisSavedObject in thisSavedObjectSet)Kinky
Yeah, good point. Back in the heady days of Nov 09 I didn't realise fast enumeration also worked for sets.Stowage
B
1

You're looking for - (void)refreshObject:(NSManagedObject *)object mergeChanges:(BOOL)flag I believe.

This will refresh the object with the info in the persistent store, merging changes if you want.

Baptism answered 10/12, 2009 at 19:13 Comment(1)
I've used this in my code. However, it in the way in which I'm using it, it doesn't seem to work.Stowage
E
2

I used the method in

http://www.mlsite.net/blog/?p=518

then every object is correctly faulted but the faults are fetch in cache so still no update

I had to do [moc stalenessInterval = 0];

And it finally worked, with relationship.

Elora answered 28/1, 2011 at 16:31 Comment(1)
Setting the stalenessInterval did it for me. Thanks!Discriminating
B
1

You're looking for - (void)refreshObject:(NSManagedObject *)object mergeChanges:(BOOL)flag I believe.

This will refresh the object with the info in the persistent store, merging changes if you want.

Baptism answered 10/12, 2009 at 19:13 Comment(1)
I've used this in my code. However, it in the way in which I'm using it, it doesn't seem to work.Stowage
A
1

I'd go with Mike's suggestion and just watch the store file for changes.

Though it may not be the most efficient, I've had success using - [NSManagedObjectContext reset] from a second process when there's a change to a store. In my case case, the code is fairly linear — all I do is run a fetch request for some data after resetting. I don't know how this will work with bindings and a complicated UI, but you may be able to post a notification to manually update things if it's not handled automatically.

Arabic answered 13/12, 2009 at 15:38 Comment(0)
P
1

I had this exact same issue with an iPhone app that I've been working on. In my case, the solution involved setting the Context's stalenessInterval to something suitably infinitesimal (e.g., 0.5 seconds).

Pleading answered 10/3, 2010 at 3:13 Comment(1)
The default is a negative value, which represents infinite staleness allowed. 0.0 represents “no staleness acceptable”: developer.apple.com/documentation/coredata/…Tanjatanjore
D
1

This works, except for sandboxes apps. You can't send a notification with a user info dict. Instead consider some other IPC like XPC or DO.

On a side note, using NSDustributedNotificationCenter is not always 100% if the system is busy.

Dieppe answered 6/12, 2011 at 21:29 Comment(0)
R
0

Setting stalenessInterval of managed object context works. My case involves multiple threads instead of process though.

Rhizocarpous answered 2/9, 2010 at 16:21 Comment(0)
T
0

Starting with iOS 9, you should now use mergeChangesFromRemoteContextSave:intoContexts:. See this for an explanation: https://www.innoq.com/en/blog/ios-writing-core-data-in-today-extension/

Tanjatanjore answered 31/10, 2022 at 8:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.