The widgets in our iOS App are custom, and therefore I added a feature to remove parts of the widget. To save settings for the widgets etc. our widgets share Core Data via App groups. However when I delete something from the widget it doesnt seem to always sync correctly. This happens primarily when the app is active in memory.
When I delete something I call this:
-(void)removeWidgetFromUser:(UserModel *)user Widget:(Widget *)widget{
if(widget != nil){
[widgetContext deleteObject:widget];
NSError *error;
if (![widgetContext save:&error]) {
NSLog(#"Unable to remove widget %#", error);
}
}
}
Then I use wormhole to sync the core data in my app and it calls this:
-(void)updateCoreData{
[self.managedObjectContext refreshAllObjects];
}
I am sure both the methods get called. But sometimes the app sees a widget I just removed, and then it also happens to reappear in my Widget.
EDIT:
I think whats happening is that the CoreData context in my app doesnt update correctly and then the widget actually syncs with the CoreData in my app. Therefore the deleted widget re-appears after some time. Still figuring it out...
I finally did it. By implementing the following code:
- (id)initWithCoder:(NSCoder *)decoder {
NSManagedObjectContext *context = [SharedCoreDataObjects sharedInstance].managedObjectContext; // use your NSManagedObjectContext
NSPersistentStoreCoordinator *coordinator = [SharedCoreDataObjects sharedInstance].persistentStoreCoordinator; //use your NSPersistentStoreCoordinator
NSURL *url = (NSURL *)[decoder decodeObjectForKey:#"URIRepresentation"];
NSManagedObjectID *managedObjectID = [coordinator managedObjectIDForURIRepresentation:url];
self = [context existingObjectWithID:managedObjectID error:nil];
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:[[self objectID] URIRepresentation] forKey:#"URIRepresentation"];
}
in my NSManagedObjects I was able use MMWormhole to send the NSManagedObjectContextDidSaveNotification
to the App and then call
[context mergeChangesFromContextDidSaveNotification:messageObject];
To let the context merge the changes. This seems to work perfectly for now!
Related
I'm trying to implement this core data stack:
PSC <--+-- MainMOC
|
+-- BackgroundPrivateMOC
There are some things I'm actually don't understand. Perhaps we have an object in our Persisten Store and we fetch it from the main MOC to do some changes (user change it manually). At the same time my BG MOC is doing some changes with the same one object and save the changes to PS. After the saving is done we must merge the BG MOC to the MAIN MOC (this is a common practice). What I expect after the merging is that the MAIN MOC contains changes from the BG MOC (because the changes were done a bit later than the MAIN ones). But this actually doesn't happened. All I have after the merging is finished is a dirty refreshedObjects = 1 in my MAIN MOC and if I fetch that object again through the MAIN MOC, I don't see any changes made through the BG MOC.
How should I correctly propagate BG changes to MAIN MOC while the
MAIN MOC was not saved prior the BG changes was made?
How to handle
the situation when my MAIN MOC has non-zero refreshedObjects after merging is completed, and
how to push these objects in the MAIN MOC to make them available to
fetch and with?
I believe my sample code can help you to understand my problem more clearly. You can just download the project (https://www.dropbox.com/s/1qr50zto5j4hj40/ThreadedCoreData.zip?dl=0) and run XCTest, that I prepared.
Here is the failing test code:
#implementation ThrdCoreData_Tests
- (void)setUp
{
[super setUp];
/**
OUR SIMPLE STACK:
PSC <--+-- MainMOC
|
+-- BackgroundPrivateMOC
*/
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
// main context (Main queue)
_mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainMOC setPersistentStoreCoordinator:coordinator];
[_mainMOC setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
// background context (Private Queue)
_bgMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_bgMOC.persistentStoreCoordinator = self.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeBGChangesToMain:)
name:NSManagedObjectContextDidSaveNotification
object:_bgMOC];
u_int32_t value = arc4random_uniform(3000000000); // simply generate new random values for the test
_mainMOCVlaue = [NSString stringWithFormat:#"%u" , value];
_expectedBGValue = [NSString stringWithFormat:#"%u" , value/2];
Earthquake * mainEq = [Earthquake MR_findFirstInContext:self.mainMOC];
if (!mainEq){ // At the very first time the test is running, create one single test oject.
Earthquake * mainEq = [Earthquake MR_createEntityInContext:self.mainMOC];
mainEq.location = nil; // initial value will be nil
[self.mainMOC MR_saveOnlySelfAndWait];
}
}
- (void)testThatBGMOCSuccessfullyMergesWithMain
{
_expectation = [self expectationWithDescription:#"test finished"];
// lets change our single object in main MOC. I expect that the value will be later overwritten by `_expectedBGValue`
Earthquake * mainEq = [Earthquake MR_findFirstInContext:self.mainMOC];
NSLog(#"\nCurrently stored value:\n%#\nNew main value:\n%#", mainEq.location, _mainMOCVlaue);
mainEq.location = _mainMOCVlaue; // the test will succeed if this line commented
// now change that object in BG MOC by setting `_expectedBGValue`
[_bgMOC performBlockAndWait:^{
Earthquake * bgEq = [Earthquake MR_findFirstInContext:_bgMOC];
bgEq.location = _expectedBGValue;
NSLog(#"\nNew expected value set:\n%#", _expectedBGValue);
[_bgMOC MR_saveToPersistentStoreAndWait]; // this will trigger the `mergeBGChangesToMain` method
}];
[self waitForExpectationsWithTimeout:3 handler:nil];
}
- (void)mergeBGChangesToMain:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
[self.mainMOC mergeChangesFromContextDidSaveNotification:notification];
// now after merge done, lets find our object with expected value `_expectedBGValue`:
Earthquake * expectedEQ = [Earthquake MR_findFirstByAttribute:#"location" withValue:_expectedBGValue inContext:self.mainMOC];
if (!expectedEQ){
Earthquake * eqFirst = [Earthquake MR_findFirstInContext:self.mainMOC];
NSLog(#"\nCurrent main MOC value is:\n%#\nexptected:\n%#", eqFirst.location, _expectedBGValue);
}
XCTAssert(expectedEQ != nil, #"Expected value not found");
[_expectation fulfill];
});
}
First, when posting core data code, I suggest you not post code that depends on a third party library, unless that third party library is directly related to your problem. I assume MR is magical record, but I don't use it, and it seems to just muddy the waters of the post because who knows what it is (or is not) doing under the covers.
In other words, try to trim examples down to as little as code as necessary... and no more... and only include third-party libraries when absolutely necessary.
Secondly, when writing unit tests for your core data usage, I suggest using an in-memory stack. You always start empty and it can be initialized however you want. Much easier to use for testing.
That said, your problem is a misunderstanding of what mergeChangesFromContextDidSaveNotification does (and does not do).
Basically, you have an object in a Core Data persistent store. You have two different MOCs attached to the store via the same PSC.
Your test then loads the object into main MOC, and changes the value without saving to the PSC. A second MOC then loads the same object, and changes its value to something different (i.e., the store, and both MOCs all have a different value for a particular attribute of the same object).
Now, when we save the MOC, if there are conflicts, the conflicts will be handled as instructed by the mergePolicy. However, the merge policy does not apply to mergeChangesFromContextDidSaveNotification.
You can think of mergeChangesFromContextDidSaveNotification as inserting any new objects, deleting any deleted objects, and "refreshing" any updated objects while preserving any local changes.
In your test, if you add another attribute (e.g., "title") and change both "title" and "location" in the BG MOC but only change "location" in the main MOC, you will see that the "title" gets merged from the BG MOC into the main MOC as expected.
However, as you note in your question, the "location" appears to not get merged. In actuality, it does get merged, but any local change will override what's in the store... and this is exactly what you want to happen because the user likely made that change, and does not want it to be changed behind their back.
Basically, any pending local changes will override changes from the to-be-merged-MOC.
If you want something different, you have to implement that behavior when you do the merge, like this...
- (void)mergeBGChangesToMain:(NSNotification*)note {
NSMutableSet *updatedObjectIDs = [NSMutableSet set];
for (NSManagedObject *obj in [note.userInfo objectForKey:NSUpdatedObjectsKey]) {
[updatedObjectIDs addObject:[obj objectID]];
}
[_mainMOC performBlock:^{
for (NSManagedObject *obj in [_mainMOC updatedObjects]) {
if ([updatedObjectIDs containsObject:obj.objectID]) {
[_mainMOC refreshObject:obj mergeChanges:NO];
}
}
[_mainMOC mergeChangesFromContextDidSaveNotification:note];
}];
}
That code first collects the ObjectIDs of each object that was updated in the merged-from-MOC.
Prior to doing the merge, we then look at each of the updated objects in the merge-to-MOC. If we are merging an object into our MOC, and our merge-to-MOC has also changed that object, then we want to allow the values in the merged-from-MOC to override those in the merged-to-MOC. Thus, we refresh the local object from the store, basically discarding any local changes (there are side effects, e.g., causing the object to become a fault, releasing references to any relationships, and releasing any transient properties - see documentation of refreshObject:mergeChanges:).
Consider the following category, which addresses your situation, and a common problem when using observers like NSFetchedResultsController.
#interface NSManagedObjectContext (WJHMerging)
- (void)mergeChangesIntoContext:(NSManagedObjectContext*)moc
withDidSaveNotification:(NSNotification*)notification
faultUpdatedObjects:(BOOL)faultUpdatedObjects
overrideLocalChanges:(BOOL)overrideLocalChanges
completion:(void(^)())completionBlock;
#end
#implementation NSManagedObjectContext (WJHMerging)
- (void)mergeChangesIntoContext:(NSManagedObjectContext *)moc
withDidSaveNotification:(NSNotification *)notification
faultUpdatedObjects:(BOOL)faultUpdatedObjects
overrideLocalChanges:(BOOL)overrideLocalChanges
completion:(void (^)())completionBlock {
NSAssert(self == notification.object, #"Not called with");
NSSet *updatedObjects = notification.userInfo[NSUpdatedObjectsKey];
NSMutableSet *updatedObjectIDs = nil;
if (overrideLocalChanges || faultUpdatedObjects) {
updatedObjectIDs = [NSMutableSet setWithCapacity:updatedObjects.count];
for (NSManagedObject *obj in updatedObjects) {
[updatedObjectIDs addObject:[obj objectID]];
}
}
[moc performBlock:^{
if (overrideLocalChanges) {
for (NSManagedObject *obj in [moc updatedObjects]) {
if ([updatedObjectIDs containsObject:obj.objectID]) {
[moc refreshObject:obj mergeChanges:NO];
}
}
}
if (faultUpdatedObjects) {
for (NSManagedObjectID *objectID in updatedObjectIDs) {
[[moc objectWithID:objectID] willAccessValueForKey:nil];
}
}
[moc mergeChangesFromContextDidSaveNotification:notification];
if (completionBlock) {
completionBlock();
}
}];
}
#end
I'm trying to separate my application work when there is a bigger work to do to optimize performance. My problem is about a NSManagedObjectContext used in another thread than the main one.
I'm calling:
[NSThread detachNewThreadSelector:#selector(test:) toTarget:self withObject:myObject];
On the test method there are some stuff to do and I have a problem here:
NSArray *fetchResults = [moc
executeFetchRequest:request
error:&error];
Here is my test method:
-(void) test:(MyObject *)myObject{
#autoreleasepool {
//Mycode
}
}
The second time I call the test method, my new thread is blocked when the executeFetchRequest is called.
This problem arrived when my test method is called more than one time in succession. I think the problem comes from the moc but I can't really understand why.
Edit:
With #Charlie's method it's almost working. Here is my code to save my NSManagedObjectContext (object created on my new thread).
- (void) saveContext:(NSManagedObjectContext *) moc{
NSError *error = nil;
if ([moc hasChanges] && ![moc save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
This method is called on the new thread. My problem now is that with this save, I have a deadlock and I don't really understand why. Without it's perfectly working.
Edit2
I'm working on this issue but I still can't fix it. I changed my code about the detachNewThreadSelector. Here is my new code:
NSManagedObjectContext* context = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = self.persistentStoreCoordinator;
context.undoManager = nil;
[context performBlock:^
{
CCImages* cachedImage;
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
childContext.parentContext = context;
cachedImage=[CCImages getCCImageForKey:path inManagedObjectContext:childContext];
UIImage *image = [self getImageFromCacheWithPath:path andCachedImage:cachedImage atDate:now];
if (image != nil){
if(![weakSelf.delegate respondsToSelector:#selector(CacheCacheDidLoadImageFromCache:)])
[weakSelf setDelegate:appDelegate.callbacksCollector];
//[weakSelf useCallbackCollectorForDelegate:weakSelf inMethod:#"initPaginatorForListMoments"];
[weakSelf.delegate CacheCacheDidLoadImageFromCache:image];
}
}
- (UIImage*) getImageFromCacheWithPath:(NSString*) path andCachedImage:(CCImages *) cachedImage atDate: (NSDate *) now{
NSURL* localURL=[NSURL URLWithString:cachedImage.path relativeToURL:[self imageCacheDirectory]];
UIImage * image;
//restore uiimage from local file system
if (localURL) {
image=[UIImage imageWithContentsOfFile:[localURL path]];
//update cache
[cachedImage setLastAccessedAt:now];
[self saveContext];
if(image)
return image;
}
return nil;
}
Just after that, I'm saving my contexts (manually for now)
[childContext performBlock:^{
NSError *error = nil;
if (![childContext save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
else{
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
}];
}
}];
There is a strange problem. My call back method is called without any problem on my controller (which implements the CacheCacheDidLoadImageFromCache: method). On this method I attest the reception of the image (DDLogInfo) and say that I want my spinner to stop. It does not directly but only 15secondes after the callback method was called.
My main problem is that my context (I guess) is still loading my image from the cache while it was already found. I said 'already' because the callback method has been called and the image was present. There is no suspicious activity of the CPU or of the memory. Instruments didn't find any leak.
I'm pretty sure that I'm using wrongly the NSManagedObjectContext but I can't find where.
You are using the old concurrency model of thread confinement, and violating it's rules (as described in the Core Data Concurrency Guide, which has not been updated yet for queue confinement). Specifically, you are trying to use an NSManagedObjectContext or NSManagedObject between multiple threads.
This is bad.
Thread confinement should not be used for new code, only to maintain the compatibility of old code while it's being migrated to queue confinement. This does not seem to apply to you.
To use queue confinement to solve your problem, first you should create a context attached to your persistent store coordinator. This will serve as the parent for all other contexts:
+ (NSManagedObjectContent *) parentContextWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)coordinator {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setPersistentStoreCoordinator:coordinator];
return result;
}
Next, you want the ability to create child managed object contexts. You will use these to perform work on the data, wether reading or writing. An NSManagedObjectContext is a scratchpad of the work you are doing. You can think of it as a transaction. For example, if you're updating the store from a detail view controller you would create a new child context. Or if you were performing a multi-step import of a large data set, you would create a child for each step.
This will create a new child context from a parent:
+ (NSManagedObjectContext *) childContextWithParent:(NSManagedObjectContext *)parent {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setParent:parent];
return result;
}
Now you have a parent context, and you can create child contexts to perform work. To perform work on a context, you must wrap that work in performBlock: to execute it on the context's queue. I do not recommend using performBlockAndWait:. That is intended only for re-rentrant methods, and does not provide an autorelease pool or processing of user events (user events are what drives nearly all of Core Data, so they're important. performBlockAndWait: is an easy way to introduce bugs).
Instead of performBlockAndWait: for your example above, create a method that takes a block to process the results of your fetch. The fetch, and the block, will run from the context's queue - the threading is done for you by Core Data:
- (void) doThingWithFetchResults:(void (^)(NSArray *results, NSError *error))resultsHandler{
if (resultsHandler != nil){
[[self context] performBlock:^{
NSArray *fetchResults = [[self context] executeFetchRequest:request error:&error];
resultsHandler(fetchResults, error);
}];
}
}
Which you would call like this:
[self doThingsWithFetchResults:^(NSArray *something, NSError *error){
if ([something count] > 0){
// Do stuff with your array of managed objects
} else {
// Handle the error
}
}];
That said, always prefer using an NSFetchedResultsController over using executeFetch:. There seems to be a belief that NSFetchedResultsController is for powering table views or that it can only be used from the main thread or queue. This is not true. A fetched results controller can be used with a private queue context as shown above, it does not require a main queue context. The delegate callbacks the fetched results controller emits will come from whatever queue it's context is using, so UIKit calls need to be made on the main queue inside your delegate method implementations. The one issue with using a fetched results controller this way is that caching does not work due to a bug.
Again, always prefer the higher level NSFetchedResultsController to executeFetch:.
When you save a context using queue confinement you are only saving that context, and the save will push the changes in that context to it's parent. To save to the store you must recursively save all the way. This is easy to do. Save the current context, then call save on the parent as well. Doing this recursively will save all the way to the store - the context that has no parent context.
Example:
- (void) saveContextAllTheWayBaby:(NSManagedObjectContext *)context {
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]){
// Handle the error appropriately.
} else {
[self saveContextAllTheWayBaby:[context parentContext]];
}
}];
}
You do not, and should not, use merge notifications and mergeChangesFromContextDidSaveNotification: with queue confinement. mergeChangesFromContextDidSaveNotification: is a mechanism for the thread confinement model that is replaced by the parent-child context model. Using it can cause a whole slew of problems.
Following the examples above you should be able to abandon thread confinement and all of the issues that come with it. The problems you are seeing with your current implementation are only the tip of the iceberg.
There are a number of Core Data sessions from the past several years of WWDC that may also be of help. The 2012 WWDC Session "Core Data Best Practices" should be of particular interest.
if you want to use managed object context in background thread, there are two approaches,
1 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set the parentContext to main thread context
2 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set persistentStoreCoordinator to main thread persistentStoreCoordinator
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
privateContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
NSManagedObjectContext *moc = mainManagedObjectContext;
if (note.object != moc) {
[moc mergeChangesFromContextDidSaveNotification:note];
}
}];
// do work here
// remember managed object is not thread save, so you need to reload the object in private context
});
before exist the thread, make sure remove the observer, bad thing can happen if you don't
for more details read http://www.objc.io/issue-2/common-background-practices.html
I'm using Core Data with UIManagedDocument for an inventory-keeping app. The problem I'm having is that the "saveToURL:..." method is actually deleting my UIManagedDocument file in the Documents directory when I save using UIDocumentSaveForOverwriting after adding an item to core data. This only happens at first launch from a new build. I created a core data/UIManagedDocument helper singleton to use throughout the app.
Here's how I initialize the UIManagedDocument instance:
#interface VDFCoreDataHelper : NSObject
#property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
#property (strong, nonatomic) UIManagedDocument *managedDocument;
#implementation VDFCoreDataHelper
- (void)createManagedDocument
{
NSURL *docsURL = [self getDocsURL];
if (![[NSFileManager defaultManager] fileExistsAtPath:[docsURL path]]) {
NSLog(#"new doc made");
_managedDocument = [[UIManagedDocument alloc] initWithFileURL:docsURL];
[self saveManagedDocumentForCreation];
[self openManagedDocument];
} else {
NSLog(#"existing doc");
_managedDocument = [[UIManagedDocument alloc] initWithFileURL:docsURL];
[self openManagedDocument];
}
}
CreateManagedDocument is called in the init method.
I have two save methods. One for creating and one for overwriting. The first one is called when I created the managed document.
At this point, I've only saved once and a UIManagedDocument directory and persistent store files exist in my documents folder.
When I want to insert an Item object (an Item entity exists), I call this method:
- (void)insertManagedObject:(NSManagedObject *)object success:(void (^)(BOOL successful))successBlock
{
NSManagedObjectContext *context = [self context];
[context insertObject:object];
NSError *error;
[context save:&error];
if (self.managedDocument.documentState == UIDocumentStateNormal) {
[self.managedDocument saveToURL:[self getDocsURL] forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success){
successBlock(success);
}];
}
}
After "saveToURL:forSaveOperation:" for over writing is called, my managed document directory and files in my Documents folder are all automatically deleted. The managedObjectContext, Item object, and managedDocument object are all valid at this point. The document's URL points to the correct destination, but all the files are gone.
After my "insertManagedObject" method is finished, I use the navigation controller to pop back to the rootViewController which contains a table view listing the items. The data that I added are kept in memory and the fetchedResultsController loads it, but the data is not saved to disk because there isn't a persistent store any longer. When I exit the app and re-enter, nothing shows up and a new managed document is created again.
This only happens if I clear the build and launch it for the first time. If I launched and immediately exit, and then enter the app again, everything works fine. It's this "saveToURL:...: method deleting my persistent store.
I've tried subclassing UIManagedDocument and logging the errors, but it doesn't show any error whatsoever. I've tried commenting out some of the code, but they don't make a difference.
If I don't use "saveToURL", the persistent store doesn't get deleted, but upon re-launch, the fetchResultsController.fetchObjects returns an empty array and tries to access a non-existent indexPath, crashing the app.
I'm considering ditching the UIManagedDocument right about now. Hopefully, someone can tell me what I may be doing wrong, or has had the same problem.
Thanks.
I was struggling with the exactly same problem as you. But wasn't finding any help...
The deleting part with no error whatsoever was driving me nuts, and I almost ditched UIManagedDocument.
But I did something that actually works!.
Actually I think the problem is trying to access a document after creating it, calling to the selector:
[_document saveToURL:self.documentURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {}];
Then all I did was after saving the document I close it, get a new Instance and then reopen it. like this:
[_coreDocument saveToURL:self.documentURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success) {
NSLog(#"Document created");
[_coreDocument closeWithCompletionHandler:^(BOOL success) {
if (success) {
NSLog(#"Closed recently created document, will open it again");
_coreDocument = nil;
_coreDocument = [[CheckinManagedDocument alloc] initWithFileURL:self.documentURL];
[_coreDocument openWithCompletionHandler:^(BOOL success) {
NSLog(#"Document oppened afer creating and closing it");
[self documentIsReadyForUse];
}];
}
}];
} else {
NSLog(#"Could not save the document at path: %#", self.documentURL.path);
}
}];
I'm writing a pretty simple application for tracking, storing, and organizing movies that are fed from iTunes and RottenTomatoes. I pull the movies down in a large back, usually ~150 movies at a time. The user can then view the feed and add movies to custom made lists.
The problem I'm running into is pulling data from the web and syncing with iCloud producing duplicates of the movies.
I've been considering breaking the movie feed into a local store (remove from iCloud) and then create the lists and other user values in either a separate iCloud Core Data store or even use iCloud key value storage (though saving movie lists will probably be rough).
This is my first app with iCloud, so I'm using some boilerplate from this objc.io article. I've done some modifications because my web syncing uses a background NSManagedObjectContext to do the work. The background MOC saves and then my main MOC merges changes in. The UI is primarily NSFetchedResultsControllers, so this entire system works really seamlessly.
Here is some code for handling iCloud NSNotifications
// happens when the main MOC saves
- (void)managedObjectContextDidSaveNotification:(NSNotification *)notification {
#synchronized(self) {
NSManagedObjectContext *moc = self.backgroundManagedObjectContext;
[moc performBlock:^{
[moc mergeChangesFromContextDidSaveNotification:notification];
}];
}
}
// happens when the bg MOC saves
- (void)backgroundManagedObjectContextDidSaveNotification:(NSNotification *)notification {
#synchronized(self) {
NSManagedObjectContext *moc = self.managedObjectContext;
[moc performBlock:^{
[moc mergeChangesFromContextDidSaveNotification:notification];
}];
}
}
- (void)persistentStoreDidImportUbiquitousContentChanges:(NSNotification *)notification {
#synchronized(self) {
NSManagedObjectContext *moc = self.managedObjectContext;
[moc performBlock:^{
[moc mergeChangesFromContextDidSaveNotification:notification];
}];
}
}
- (void)storesWillChange:(NSNotification *)notification {
NSManagedObjectContext *moc = self.managedObjectContext;
[moc performBlockAndWait:^{
NSError *error = nil;
if ([moc hasChanges] && [moc save:&error]) {
DDLogInfo(#"MOC saved succesfully");
}
else {
DDLogError(#"Error saving MOC: %#",error);
}
}];
}
- (void)storesDidChange:(NSNotification *)notification {}
I originally thought I could add some logic in once iCloud finished merging changes, but that doesn't look very reliable as I could end up in a loop of changes between devices.
Any thoughts on how this could be made possible? I'm sure people have done this before, I just can't find any helpful resources on it.
If I understand your situation properly, it is possible that the user downloads the same movie on two different devices at about the same time, and after syncing up, you have a duplicate of the movie.
Apple's advice for this situation is simply to de-dupe after any iCloud merge. You do a fetch looking for objects with the same movie id, and you sort the duplicates in a deterministic way, and delete one. If you don't sort them, you might delete one object on one device, and a different object on the other device, and end up with no objects.
This article has efficient code for seeking duplicates: http://www.atomicbird.com/blog/icloud-complications-part-2
If you don't like this whole post-merge de-duplication, frameworks like TICDS and Ensembles allow you to provide global ids, which remove the necessity to de-dupe. (Disclosure: I develop Ensembles)
I am working an iPhone app and a Mac app that use Core Data.
I would like to have these 2 apps synchronise their databases via iCloud storage.
I have made adjustments to the implementations of the managedObjectContext & persistentStoreCoordinator & added mergeiCloudChanges - from the updated Recipes example code:
#pragma mark -
#pragma mark Core Data stack
// this takes the NSPersistentStoreDidImportUbiquitousContentChangesNotification
// and transforms the userInfo dictionary into something that
// -[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:] can consume
// then it posts a custom notification to let detail views know they might want to refresh.
// The main list view doesn't need that custom notification because the NSFetchedResultsController is
// already listening directly to the NSManagedObjectContext
- (void)mergeiCloudChanges:(NSNotification*)note forContext:(NSManagedObjectContext*)moc {
NSLog(#"merging iCloud stuff");
[moc mergeChangesFromContextDidSaveNotification:note];
NSNotification* refreshNotification = [NSNotification notificationWithName:#"RefreshAllViews" object:self userInfo:[note userInfo]];
[[NSNotificationCenter defaultCenter] postNotification:refreshNotification];
}
/**
Returns the managed object context for the application.
If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
*/
- (NSManagedObjectContext *)managedObjectContext
{
if (managedObjectContext != nil)
{
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
if (IOS_VERSION_GREATER_THAN_OR_EQUAL_TO(#"5.0")) {
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[moc performBlockAndWait:^{
[moc setPersistentStoreCoordinator: coordinator];
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(mergeChangesFrom_iCloud:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:coordinator];
}];
managedObjectContext = moc;
} else {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator:coordinator];
}
}
return managedObjectContext;
}
// NSNotifications are posted synchronously on the caller's thread
// make sure to vector this back to the thread we want, in this case
// the main thread for our views & controller
- (void)mergeChangesFrom_iCloud:(NSNotification *)notification {
NSManagedObjectContext* moc = [self managedObjectContext];
// this only works if you used NSMainQueueConcurrencyType
// otherwise use a dispatch_async back to the main thread yourself
[moc performBlock:^{
[self mergeiCloudChanges:notification forContext:moc];
}];
}
/**
Returns the managed object model for the application.
If the model doesn't already exist, it is created by merging all of the models found in the application bundle.
*/
- (NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel != nil) {
return managedObjectModel;
}
managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];
return managedObjectModel;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator__ != nil) {
return persistentStoreCoordinator__;
}
// assign the PSC to our app delegate ivar before adding the persistent store in the background
// this leverages a behavior in Core Data where you can create NSManagedObjectContext and fetch requests
// even if the PSC has no stores. Fetch requests return empty arrays until the persistent store is added
// so it's possible to bring up the UI and then fill in the results later
persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
// prep the store path and bundle stuff here since NSBundle isn't totally thread safe
NSPersistentStoreCoordinator* psc = persistentStoreCoordinator__;
NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:#"MyApp.sqlite"];
// do this asynchronously since if this is the first time this particular device is syncing with preexisting
// iCloud content it may take a long long time to download
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *storeUrl = [NSURL fileURLWithPath:storePath];
// this needs to match the entitlements and provisioning profile
NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:nil];
NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:#"MyApp"];
cloudURL = [NSURL fileURLWithPath:coreDataCloudContent];
NSLog(#"cloudURL: %#", cloudURL);
// The API to turn on Core Data iCloud support here.
NSDictionary* options = [NSDictionary dictionaryWithObjectsAndKeys:#"xxxxxxxx.com.me.MyApp",
#"MyApp",
cloudURL,
NSPersistentStoreUbiquitousContentURLKey,
[NSNumber numberWithBool:YES],
NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES],
NSInferMappingModelAutomaticallyOption,
nil];
NSError *error = nil;
[psc lock];
if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
Typical reasons for an error here include:
* The persistent store is not accessible
* The schema for the persistent store is incompatible with current managed object model
Check the error message to determine what the actual problem was.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[psc unlock];
// tell the UI on the main thread we finally added the store and then
// post a custom notification to make your views do whatever they need to such as tell their
// NSFetchedResultsController to -performFetch again now there is a real store
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"asynchronously added persistent store!");
[[NSNotificationCenter defaultCenter] postNotificationName:#"RefetchAllDatabaseData" object:self userInfo:nil];
});
});
return persistentStoreCoordinator__;
}
I can see files appear in my "/Users/me/Library/Mobile Documents" directory when i build/run myapp.
But I have no idea if it is syncing over to the iCloud storage - and obviously the data between the iphone and mac is not synced.
Are there other methods I need to implement to make the data move to the cloud?
And is there any way for me to view what documents are actually on the iCloud storage?
Here is a quick partial answer.
You can see what is stored in iCloud:
On the Mac:
System Preferences.app -> iCloud -> click on 'Manage...' you will then see a list of all apps that have documents stored Mac OS X or iOS.
On iOS:
Preferences -> iCloud -> Archive & Backup -> option below Space used you will then see a list of all apps that have documents stored Mac OS X or iOS.
As long as you are using NSFileManager's setUbiquitous: itemAtURL: destinationURL: error:the documents should be getting sent to iCloud for you and showing up on other devices.
Another partial answer. Take a look at : http://www.raywenderlich.com/6015/beginning-icloud-in-ios-5-tutorial-part-1 if you have not yet. I am working on the same thing as you namely, Mac App and iOS
app sharing data. Good Luck. Mark
I just learned about : http://mentalfaculty.tumblr.com/archive
Look for "Under The Sheets" CoreData and iCloud. Check it out!
OK my code look a bit different, I have it in a separate class to reuse it for all my projects. Nevertheless if iCloud is enabled (URLForUbiquityContainerIdentifier:nil return not nil) I setup my NSPersistentStoreCoordinator like this:
// ---- iCloud Setup
// fist container in entitlements
NSURL *iCloudDirectoryURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
// if iCloud is enabled setup the store
if (iCloudDirectoryURL) {
__iCloudEnabled = true;
NSLog(#"iCloud:%#", [iCloudDirectoryURL absoluteString]);
// AppDelegate has to provide the contentnamekey
NSString *contentNameKey = [appDelegate dataStoreContentNameKey];
options = [NSDictionary dictionaryWithObjectsAndKeys:contentNameKey, NSPersistentStoreUbiquitousContentNameKey, iCloudDirectoryURL, NSPersistentStoreUbiquitousContentURLKey, nil];
}
I miss where you setup the NSPersistentStoreUbiquitousContentNameKey.
The second is clear i guess, this works only on the device and your App ID need iCloud enabled.