This is my app's processContentChanges: method, which is triggered by NSPersistentStoreDidImportUbiquitousContentChangesNotification:
- (void)processContentChanges:(NSNotification *)notification {
[self.managedObjectContext performBlock:^{
// Merge incoming data updates in the managed object context
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
// Post notification to trigger UI updates
#warning What do I actually do here?
}];
}
I'm using NSFetchedResultsControllers throughout my app so that the UI updates automatically when changes are received from another device through iCloud. This all seems to work, but the comment saying // Post notification to trigger UI updates was there in the template method already. Am I actually supposed to do something here, or can I safely leave things the way they are?
Well, although I haven't had confirmation of this I don't think there is anything else that needs to be done in this method as long as the following criteria are met:
You implement NSPersistentStoreDidImportUbiquitousContentChangesNotification correctly as per the template so that new content is merged to the managed object context
Your content is generated using NSFetchedResultsController objects
Your viewControllers conforms to the the NSFetchedResultsControllerDelegate protocol, and implements controllerWillChangeContent:, controllerDidChangeContent: and controller:didChangeObject:atIndexPath:forChangeType:newIndexPath
In those methods, update your views accordingly to display new content, remove deleted content, and update changed content.
If you have any objects which utilise CoreData without an NSFetchedResultsController then you might need to update these by manually re-fetching the data when NSPersistentStoreDidImportUbiquitousContentChangesNotification is posted by CoreData.
Related
I'm trying to implement some basic UIManagedDocument import/export functionality into my app, mainly for dev so that I can easily inspect the document contents and more crucially preserve a set of test data when I start trying to iterate on my CoreData models.
All I am trying to do is load some JSON data from a local file and inject it into my apps UIManagedDocument. The UIManagedDocument's ManagedObjectContext contents are visualised in my app using some Core Data Table View Controllers from the Stanford iOS courses.
I thought I'd try to write this with some threading to keep the UI responsive and to learn how to do it. So I've done something like this
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// read JSON data file
// parse JSON into dictionaries etc
// iterate over dictionaries
// try to create a new core data entity with my document's ManagedObjectContext
} );
At first I thought this was working. No errors, asserts, crashes triggered and upon looking at my CoreData TableViews I could see the newly added objects in my UI. Unfortunately the newly added objects were seemingly never saved back to the store. I even hooked up to listen to the NSManagedObjectContextDidSaveNotification from my UIDocument's managedObjectContext and saw it wasn't triggering on pressing the home button, like it usually does if it has some changes performed in the app with my UI pending. Infact even doing these operations in the UI wouldn't cause the notification and saving to occur so it was clearly not happy.
I unrolled the code from within the background queue and ran it on the main thread synchronously and everything worked ok, the new data was saved correctly.
I started reading about the complexities of threading and coredata, the documentation seemed to suggest using the UIDocument's ManagedObjectContext's parent ManagedObjectContext to perform operations on in the background so I tried doing the same code again using this parent context, so as follows
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// read JSON data file
// parse JSON into dictionaries etc
// iterate over dictionaries
// try to create a new core data entity with my document's ManagedObjectContext parent ManagedObjectContext
} );
This time for some reason the CoreData TableView controllers no longer updated to show the newly injected objects. Even after explicitly calling save on the parent context, nothing appeared. However on quitting the app and reloading the app the newly injected objects did seem to be added correctly. Interestingly at this point i'd left a fetchrequest with a cachename specified and that threw up an error on this first run of the app after injecting the objects this way. I guess somehow the way the object had come from the parent context directly perhaps invalidated the cache somehow, that's still something I don't fully understand. Even changing the cache to nil didn't fix the issue of the table views not updated the same session as when the objects were injected into the parent context.
Looking elsewhere I've seen some uses of the managedObjectContext performBlock suggested. Another case where someone has said you must call
[document updateChangeCount:UIDocumentChangeDone]
after all changes to ensure the saving is performed, or perhaps using
- (void)autosaveWithCompletionHandler:(void (^)(BOOL success))completionHandler
instead. Though elsewhere I've seen mentioned that saving should be enough to push context contents through the hierarchy. Does saving only work from child -> parent and not from parent -> child.
Or am I just doing it wrong?
Anyone's time and help is really appreciated! Cheers.
please look at the 'Parent/Child Contexts' section in the Multi-Context CoreData.
Whenever a child MOC saves the parent learns about these changes and this causes the fetched results controllers to be informed about these changes as well. This does not yet persist the data however, since the background MOCs don’t know about the PSC. To get the data to disk you need an additional saveContext: on the main queue MOC.
I do saving to the PSC n following way:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
{
// read JSON data file
// parse JSON into dictionaries etc
// iterate over dictionaries
NSError* error;
if (self.managedObjectContext.hasChanges)
{
[self.managedObjectContext save: &error];
}
// Save main context
dispatch_sync(dispatch_get_main_queue(), ^
{
[[AXContextHelper sharedInstance] saveMainContext];
});
}
After looking into all the suggestions I could find for similar problems, none of them seemed to help.
The scenario i was describing in the end had the store stuff to file handled ok, but the UI not updating. At the time when I do this import/export I'm in a view ontop of the core data table view controller that doesn't update when I inject all these JSON objects, so in the end all I did was force that controller to re-fetch it's data. I believe the NSFetchedResultsController is meant to monitor the managedObjectContext and update the fetch request as required. For whatever reason, this wasn't working.
With the force re-fetch called, everything seems to work ok. The newly injected entities appear in my tables, and are saved to the store files.
Either there are some bugs in these objects, or I'm still using it wrong but I see other problems... my import/export data both work now on background threads with the parent context. What I've noticed today is that I can use the UI to properly insert and edit some objects using the child context on the main thread. I don't deliberately call anything at the moment after making these edits so i guess the edits are are still pending till core data decides to save. If i then go to my export feature and use it using the parent context then i find the newly edited or inserted objects aren't there.
Clearly there's no messaging happening in time from child -> parent, and messaging from background thread edits on the the parent itself just doesn't seem to work.. I never get any notifications for the edits i perform in a thread on the parent context, even though they seem to work. If I force the save to happen via the documents then the passed in dictionaries show no edits being made, even though the edits are saved correctly to store files somehow.
I guess i now need to force my document to save after every operation, or at least before every operation i'm about to do on another thread to ensure the parent/child are both in sync.
Still the problem I originally described I've managed to get around, but I'm still left wondering quite how I should be using the parent and child context of a UIManagedDocument to avoid encountering these issues.
I'm looking to integrate iCloud with a Core-Data-managed SQLite database (only on iOS 7 and later). I've been reading Apple's guide on using Core Data with iCloud (https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/UsingCoreDataWithiCloudPG/UsingCoreDataWithiCloudPG.pdf).
To quote from the guide, "Core Data posts an NSPersistentStoreCoordinatorStoresWillChangeNotification notification. In your notification handler, you reset your managed object context and drop any references to existing managed objects."
Calling -reset on the MOC to reset it isn't the problem, the problem is the part where they say all references to managed objects need to be dropped. I understand why this needs to be done (because the persistent store is changing), what I don't know is how to do it.
All my Core Data work is handled by a singleton and I had originally thought of posting a notification, and listening classes could set all their managed objects to nil. First, this doesn't sound like a particularly good way of doing it. Secondly, I have a FetchedResultsController managing a tableView, the FetchedResultsController manages it's own managed objects, therefore, as far as I know, I can't set them to nil.
I'd be really grateful for any advice on what to do here.
Thanks in advance.
The way I handle situations like this is to post two notifications in my app: just before resetting, and just after resetting.
For example, I might post MYMainContextWillResetNotification, then reset the context, then post MYMainContextDidResetNotification.
Any controller receiving the will-reset notification should release its managed objects, but also store any information it will need to recover after the reset. Usually this will be one or more NSManagedObjectID objects. In some cases, you may not need to store anything, simply performing a fetch after the reset instead.
A typical method might look like this:
- (void)mainContextWillReset:(NSNotification *)notif
{
self->noteID = note.objectID;
}
This code supposes there is a controller for a single note object. When the reset is about to take place, the note's object identifier is stored in an instance variable.
The did-reset notification method retrieves the note.
- (void)mainContextDidReset:(NSNotification *)notif
{
note = [context existingObjectWithID:noteID error:NULL];
[self refreshViews];
}
This code uses existingObjectWithID:error:, but you could equally do a fetch.
With an NSFetchedResultsController, you would need to call performFetch: in the did-reset method, to refresh the objects.
Imagine a social photo app like Instagram. You see one of your friend's photos in your feed. That photo is persisted in CoreData as something like this:
Photo (NSManagedObject)
Attributes: id, imageURL
Relationships: comments (set of Comment objects), likes (set of Like objects)
I have a view controller that has a reference to the Photo object. This view controller also handles actions for liking and commenting on the photo.
So, when someone likes a photo the flow is this: Send the like to the server. If the API post is successful, update the Photo object in CoreData with any new information received from the server, which will include the new like. At this point the Photo should have one more Like object related to it in CoreData than before I pressed the like button.
Now here is where I'm having a problem. The view controller needs to update the number of likes on the photo. In a success block, I'm doing this:
self.likesLabel.text = [NSString stringWithFormat:#"%d likes", self.photo.likes.count];
The problem is that self.photo.likes.count is reporting 0 at this point (it was non-zero before I pressed the like button). If I navigate elsewhere and come back to this screen, the count will update properly. So it seems that the Photo managedObject becomes "dirty" when I update it. The update probably happens in another context (I use Magical Record). The update looks something like this:
NSManagedObjectContext* context = [NSManagedObjectContext MR_contextForCurrentThread];
Photo* photo = [Photo MR_findFirstWithPredicate:somePredicate inContext:context];
...
photo.likes = likes;
photo.comments = comments;
[photo save:context];
So the question is, how do I properly keep the Photo object updated in the view controller so that I can always reliably query the number of likes and comments? In general, how can you always keep a fresh version of an NSMangagedObject, given that it may be updated in other threads/contexts?
I have tried listening for the NSManagedObjectContextObjectsDidChangeNotification, but I run into the same problem where the Photo reports 0 likes and comments when I get that notification.
One solution for this scenario I frequently use is KVO. In your case, I would observe the "likes" property on the managed object. You can look up standard KVO articles on how to do that properly. However, this is only half the story. So, in order for those values to change for you properly, the NSManagedObjectContext that is "managing" that property needs to get updates itself. This is where either merging saves on NSManagedOjbectContextDidSaveNotification events in the case of a thread isolation context, or the parent/child merge rules come into play. The point is, you could be making the change to the actual data on any context within your app. It's up to you to merge in those changes into the context you currently care about (probably what's displaying on the screen).
All that said, MagicalRecord should have some build in methods to already handle that half of the problem for you. If not, make sure your context that is displaying use information is being updated properly when a background context is being saved. The thing to remember is that you have to update the context, and once the context merges in the new changes on update, then your KVO methods will fire with the proper values.
listen and apply merges made in other contexts
#pragma mark - Core Data stack
- (void)mergeChanges:(NSNotification *)notification {
// Merge changes into the main context on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
[[NSNotificationCenter defaultCenter] postNotificationName:MyMainContextMergedChangesNotification object:nil];
});
}
after the merge you can refresh the GUI
alternativly/additionally you can force the context to refresh the object from the db calling refreshObject:
I want my NSManagedObject to listen for a notification from a timer class that will post an NSnotification every second. This is needed to update a value in my NSManagedObject.
Problem is as the CD lifecycle is out of my control it appears that i'm getting duplicate NSNotifications which I have found out is due to the multiple contexts that a NSManagedObject could be in.
How canI listen for this notification reliably inside my NSManagedObject?
This is a normal side effect of the way Core Data works. You're creating multiple objects that represent the same underlying data. All of them are registering for the same notification, so all of them get it. Listening for a notification like this is not a very good idea, because this duplication is a fundamental part of how the system works.
If the objects that should respond to the notification all come from the same managed object context, there are workarounds. For example, to listen for the notification only if the object was fetched from the root context in a parent/child context setup, do something like
if ([[self managedObjectContext] parentContext] == nil) {
...register for notification
}
If you don't use parent/child context relationships, you could decide that one specific context is "the one" whose managed objects get the notification, and compare [self managedObjectContext] to that.
A better solution would be to sidestep the problem and listen for the notification somewhere else-- or else just update the value from the timer callback, without using a notification. Whenever the timer fires, update the value on one specific instance of the object. This way you'll know that you're making the change in exactly one place, on one object. Other instances from other contexts would need to merge the change to get the new value.
My program does work like link below:
Update results of NSFetchedResultsController without a new fetch
show result of NSFetchedResultsController to UITableView
get new object from web service and store it to core data (in same view controller, with RestKit)
update table view with notification of NSFetchedResultsController delegate
The implementation of NSFetchedResultsControllerDelegate is copied from Apple's Core Data project and my predicated is:
[NSPredicate predicateWithFormat:#"isMyTest == TRUE"]
If the property update goes from TRUE to FALSE, it removes rows from the tableview (because the object for the row is in fetchedObjects of the NSFetchedResultsController)
However, if the property update goes from FALSE to TRUE, the NSFetchedResultsController notifies nothing, so the new data cannot be seen in table view. If I update BOTH NSFetchedResulsController and UITableView manually, it shows the new data.
I thought NSFetchedResultController watches all changes in persistent store, is it too big hope? :D
(I really want to do that because other view controller can update persistent store, then it is hard to update the view controller.)
If so, can you let me know how can I update NSFetchedResultsController in beautifully way?
(update)
in reference of NSfetchedResultsController, I read words below:
A controller thus effectively has three modes of operation, determined
by whether it has a delegate and whether the cache file name is set.
No tracking: the delegate is set to nil. The controller simply
provides access to the data as it was when the fetch was executed.
Memory-only tracking: the delegate is non-nil and the file cache name
is set to nil. The controller monitors objects in its result set and
updates section and ordering information in response to relevant
changes.
Full persistent tracking: the delegate and the file cache name are
non-nil. The controller monitors objects in its result set and updates
section and ordering information in response to relevant changes. The
controller maintains a persistent cache of the results of its
computation.
"Full persistent tracking" does not mean what I want? I set up cacheName, but it works same.
I thought NSFetchedResultController watches all changes in persistent
store, is it too big hope?
The FRC only automatically observes the objects returned by its fetch. This is normally not a problem as changes to objects monitored by an FRC should arise from either the tableview UI or a background context. In the former, the FRC is alerted to the change and in the latter, merging the foreground and background context will trigger the FRC to update.
It sounds like you've got code changing values but you're not notifying the FRC that you've made any changes. (If you got a tableview displaying all objects whose isMyTest == TRUE then obviously you can't access objects from the UI whose isMyTest == FALSE.) In that case, you need to register the tableview controller for:
NSManagedObjectContextObjectsDidChangeNotification
… notifications from the context so that you can tell the FRC to update for changes made outside its observation.
I'm sorry to my mistake, I tested with new test project.
CoreData DOES full-tracking of whole persistent store.
This means, if new object that is suitable to predicate of NSFetchedResultsController, delegate will notify it.