Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSManagedObject Not Reflecting Changes After Background Thread NSManagedObjectContextDidSaveNotification

I am having trouble with an NSManagedObject not reflecting the changes made to a persistent store after a background thread has saved it's context.

The Setup

In a simple test application I have a single window that lists all of the objects in my core data persistent store, a search box to filter the results and a text field to show the name of the selected item and allow the name to be changed.

Bindings are as follows:

ArrayController --> AppDelegate --> ManagedObjectContext
TableView Col 1 --> ArrayController --> values --> arrangedObjects.widgetName
TableView Col 2 --> ArrayController --> values --> arrangedObjects.uid

SearchField --> ArrayController --> predicate --> filterPredicate

TextField --> ArrayController --> value --> selection.widgetName

I also have a button that starts a background (NSOperation) fetch of data from a web server.

The Process

When the user clicks the refresh button, an NSOperation is kicked off that goes and grabs the widgets asynchronously, parse the response, checks for local widgets to delete that are not in the response, new widgets to add that are not stored locally and existing local widgets that should be updated with the data retrieved form the server.

Once the processing has finished, the main context is notified using:

[mainContext performSelectorOnMainThread:
       @selector(mergeChangesFromContextDidSaveNotification:) 
                              withObject:notification 
                           waitUntilDone:YES];

I have an observer in the main controller for testing that shows the changes went through just fine and the main controller got notified.

The Problem

If I make a change to a selected object using the text field, when the data on the background thread is saved, the object in the UI is not updated to reflect those changes (i.e. it doesn't overwrite the UI with the changes from the server).

For example, given the following three widgets and ID's:

Test Name 1 | ID 123
Test Name 2 | ID 234
Test Name 3 | ID 345

If I change the name in the UI of Test Name 2 to Renamed 2 I have the following:

Test Name 1 | ID 123
Renamed 2   | ID 234
Test Name 3 | ID 345

When I refresh on the background, I want the list to reflect the server's state, ie go back to:

Test Name 1 | ID 123
Test Name 2 | ID 234
Test Name 3 | ID 345

Instead it remains:

Test Name 1 | ID 123
Renamed 2   | ID 234
Test Name 3 | ID 345

I know the persistent store was updated because if I kill the app from XCode and relaunch, the desired information is displayed. If I quit the app normally, the changed value is written to the store on application close and reopening shows the renamed value.

What I've Tried

I know the message is being sent form the background to the main context and I know the data is being persisted to the store. Therefore, the issue I believe is that the main context is not merging as I would expect it to, or I need to somehow force the array controller to fetch from the persistent store and discard it's context.

  • I have tried processPendingChanges: upon notification of the store save, but I suspect I am just writing the Renamed 2 to the store.
  • I have tried doing a rearrangeObjects on the array controller, but as the array controller is dealing with the main context I suspect this is doing nothing
  • I have tried doing a fetch:nil on the array controller to do a fetch from the persistent store, but once again I suspect that the main context is overwriting the value of Renamed 2 because it is not yet saved.
  • I have tried fetchWithRequest:nil merge:NO error:&error on the array controller as per the Apple docs but still this does not seem to change the displayed value

What I think needs to happen is for the array controller to save its data down to the persistent store before I write the background store data so that a fetch on the array controller will cause the data to be accurate as per my expectations. And if this is indeed the case, how would I tell the array controller to do this, or the would the array controller simply know of the changes through bindings if the main managedObjectContext was saved somehow?

I can hack the solution by doing a fetch from the persistent store, putting that data in an array and doing a setContent: on the array controller and then repeating this when the persistent store is saved, but this feels simply wrong, not to mention the issue of then having to track the selected state of the array controller (and potentially any sub-array selections that might be happening as a result of that primary selection).

Am I off base? I'm obviously missing something here.

Any words of wisdom or advise would be very much appreciated.

like image 280
Hooligancat Avatar asked Mar 25 '11 20:03

Hooligancat


1 Answers

Ah the power of frustration coupled with determination. I'm not sure that this is necessarily the best or recommended approach, but it certainly serves my needs.

My goal was to have any non-persisted changes either persist before a background update is saved, or be discarded before a background update is saved. Both serve the same purpose in my world (the server data is always right).

Turns out I simply needed to add an observer in my NSOperation:

NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self 
       selector:@selector(prepareMerge:) 
           name:NSManagedObjectContextWillSaveNotification object:ctx];

Which calls a method in the operation:

-(void)prepareMerge:(NSNotification *)notification {

    [[NSNotificationCenter defaultCenter] 
         postNotificationOnMainThreadName:@"SaveNow"
                                   object:nil];
}

The notification is sent to the main thread (courtesy of a category on NSNotificationCenter posted over at cocoanetics.com) which is listened for in a relevant class on the main thread:

[[NSNotificationCenter defaultCenter] addObserver:self 
                                         selector:@selector(saveNow:) 
                                             name:@"SaveNow" 
                                           object:nil];

And of course the method that actually makes the change:

-(void)saveNow:(NSNotification *)aNote {

    [[self managedObjectContext] rollback];
}

The array controller and any other UI components that have updated values rollback right before the save gets committed. The save goes through and the old local values are all replaced with new values.

Job done.

like image 152
Hooligancat Avatar answered Oct 04 '22 02:10

Hooligancat