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.
processPendingChanges:
upon notification of the store save, but I suspect I am just writing the Renamed 2
to the store.rearrangeObjects
on the array controller, but as the array controller is dealing with the main context I suspect this is doing nothingfetch: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.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 valueWhat 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With