I have been using iCloud in my apps for a while now and am recently having an issue where devices refuse to talk to each other. Or at least that's what I thought until I started logging the methods where the merging takes place. My persistentStoreCoordinator is set up as described in a previous question of mine.
The problem is the following. When setting up my managedObjectContext, I add an observer to it to view the NSPersistentStoreDidImportUbiquitousContentChangesNotification notification like follows:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChangesFrom_iCloud:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:coordinator];
where coordinator is the set up as NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator].
My iCloud methods are as follows:
- (void)mergeiCloudChanges:(NSNotification *)note forContext:(NSManagedObjectContext *)moc {
NSLog(#"insert %#", [[note userInfo] valueForKey:#"inserted"]);
NSLog(#"delete %#", [[note userInfo] valueForKey:#"deleted"]);
NSLog(#"update %#", [[note userInfo] valueForKey:#"updated"]);
[moc mergeChangesFromContextDidSaveNotification:note];
NSNotification *refreshNotification = [NSNotification notificationWithName:#"RefreshAllViews" object:self userInfo:[note userInfo]];
[[NSNotificationCenter defaultCenter] postNotification:refreshNotification];
}
- (void)mergeChangesFrom_iCloud:(NSNotification *)notification {
NSLog(#"merging changes");
NSManagedObjectContext *moc = [self managedObjectContext];
[moc performBlock:^{
[self mergeiCloudChanges:notification forContext:moc];
}];
}
So now we have the actual problem
At first, syncing went flawlessly. Data from one device was changed, then that change appeared on the other device. But now, nothing. The curious part is in those logs you see in mergeiCloudChanges:forContext:. The merge actually occurs. I see this triggered. But the insert, delete, and update parts of the merge note are ALWAYS without content. So the merge occurs, but with no data. For the life of me, I cannot figure out why this is or how to get it to sync properly again. Any help would be more than appreciated.
Note: I am also using the NSUbiquitousKeyValueStoreDidChangeExternallyNotification notification for my NSUbiquitousKeyValueStore (which I use to sync the NSUserDefault key-values), and those notifications transfer over and update without any hiccups at all, which confuses me even more as to why the persistentStoreCoordinator notifications are blank.
I truly hope someone has seen/experienced this before because after two weeks exhausting every avenue I can think of to find and fix this on my own, I feel I am now at a standstill.
I've got this one figured out. Basically, the transaction logs appeared to be corrupted or misplaced. I have reworked a way to resycn devices and will be posting the results immediately on a previous question of mine
Related
I'm writing an app that uses Core Data and is synced with iCloud. To do this, I have a UIManagedDocument that I set up as shown below:
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:[self iCloudStoreURL]];
document.persistentStoreOptions = #{NSPersistentStoreUbiquitousContentNameKey: [document.fileURL lastPathComponent], NSPersistentStoreUbiquitousContentURLKey: [self iCloudCoreDataLogFilesURL], NSMigratePersistentStoresAutomaticallyOption: #YES, NSInferMappingModelAutomaticallyOption : #YES};
self.mydoc = document;
[document release];
[document.managedObjectContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentContentsChanged:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:document.managedObjectContext.persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentStateChanged:) name:UIDocumentStateChangedNotification object:document];
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.mydoc.fileURL path]]) {
// does not exist on disk, so create it
[self.mydoc saveToURL:self.mydoc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self populateTable];//synchronous call. few items are added
[self iCloudIsReady];
}];
} else if (self.mydoc.documentState == UIDocumentStateClosed) {
// exists on disk, but we need to open it
[self.mydoc openWithCompletionHandler:^(BOOL success) {
[self iCloudIsReady];
}];
} else if (self.mydoc.documentState == UIDocumentStateNormal) {
// already open and ready to use
}
}
My issue with this approach is that I keep getting "Optimistic locking failure" when running the app in two devices. I read in Apple's Core Data documentation that a way to "avoid" this kind of issue was to set up the merge policy to NSMergeByPropertyObjectTrumpMergePolicy, something that I am already doing but for some reason is not working.
One thing I can't find is how to fix this. For example, if this is something that could happen, my app should be at least aware and prepared to handle this behavior. But I have no idea on how to handle this. For example, how do I get the conflicting objects and resolve them? Because every time this failure happens, I start getting UIDocumentStateSavingError when trying to save the document and the only way to stop getting this error is by killing the app and re-launching it.
I finally figured this out (at least, I think I did). Apparently, on iOS6+ (I have no idea about iOS5) UIManagedDocument takes care of all the merging for you. So, the observer below, which was only responsible for calling "mergeChangesFromContextDidSaveNotification:" was in fact merging what was just merged.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentContentsChanged:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:document.managedObjectContext.persistentStoreCoordinator];
...
- (void)documentContentsChanged:(NSNotification *)notification {
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
Calling "mergeChangesFromContextDidSaveNotification:" was the line responsible for triggering the "Optimistic locking failure". I removed it and everything started working as expected. Unfortunately this only lasted a couple of hours. Now I keep getting the "iCloud Timed Out" error, but this one I'm sure it's Apple's fault.
Anyway, after a ton of bugs and three different iCloud + Core Data approaches, I think I will hold on integrating iCloud into my app. It is far too unstable and buggy. I really wish Apple could have fixed this with iOS6, iCloud + Core Data is a very powerful tool, unfortunately, it is not ready yet.
Thanks to everyone who tried to help.
I am using Core Data and was wondering if I am doing things correctly. I am opening my UIManagedDocument from a singleton object using the completion handler and block below.
[[self managedDocument] openWithCompletionHandler:^(BOOL success) {
if(success) {
NSLog(#"DOCUMENT: Success, Opened ...");
// TODO: Things to do when open.
// ...
// ...
}
}];
On my UIViewController I have setup an observer to watch for a UIDocumentStateChangedNotification to indicate that I can start working with the document.
- (void)awakeFromNib {
NSLog(#"%s", __PRETTY_FUNCTION__);
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(documentIsReady) name:UIDocumentStateChangedNotification object:nil];
}
This seams to work just fine, but I am conscious that I am not using the callback block. One solution might be to create my own notification and post that from the block, it does the same thing essentially but just makes the code more obvious to read. Any comments would be much appreciated.
I'd say that if you only need to notify one controller, once, and only when the document is opened (you have an app that uses a single UIManagedDocument that gets passed between controllers, like the CS193P demo), it would be better to leave only the code inside the completion block.
However, if your app is going to open and close the document many times, and multiple controllers have to be aware of that change, you should use notifications.
If an entry is changed in core data on another device, NSLog messages show that it's noticed the changes, but NSPersistentStoreDidImportUbiquitousContentChangesNotification is not being called. It takes until a save on my first device for it to know to update my tableview.
This is my code:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(iCloudUpdates:)
name:NSPersistentStoreDidImportUbiquitousContentChangesNotification
object:nil];
Anybody know why this might not be working?
First option:
If your problem is a delay in updating both devices, that is normal behavior, Apple doesn't guarantee update timing, but it is usually fast.
If on the other side the problem is that iCloudUpdates method is not called make sure that signature is correct, should be :
-(void)iCloudUpdates:(NSNotification*)notification {
// do your stuff here
}
Second option:
at time of writing, iOS 5 has big big problem with iCloud and CoreData, I recently shipped my application without iCloud support.
If you want to know what is going on turn logging on for both CoreData and iCloud by putting:
-com.apple.CoreData.SQLDebug 1
-com.apple.coredata.ubiquity.logLevel 3
in your scheme manager, under run->arguments tab.
If you see some strange errors in 'destination' device that's the case of iCloud not working for being bugged.
I think the problem with your code is that in the "addObserver" you've set the object to nil. The object should be your persistentStoreCoordinator as shown below.
__weak NSPersistentStoreCoordinator *psc = self.context.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(iCloudUpdates:)
name:NSPersistentStoreDidImportUbiquitousContentChangesNotification
object:psc];
I am writing an iPhone app in which I have three countdown timers, that each set a local notification when they are started by the user (IBAction).
My question is, how can I reference to an existing local notification elsewhere in the code? I have tried referencing it directly like this which crashes:
[[UIApplication sharedApplication] cancelLocalNotification:myNotification];
And like this after scheduling the notification and adding it to the user defaults:
In my scheduling method...
[myDefaults setObject:myNotification forKey:#"myNotification"];
...
And in my cancelling method...
NSUserDefaults *myDefaults = [NSUserDefaults standardUserDefaults];
[[UIApplication sharedApplication] cancelLocalNotification:[myDefaults objectForKey:#"myNotification"]];
[myDefaults synchronize];
My app crashes with a SIGABRT error on the cancelLocalNotification line above. Can anybody tell me where I am going wrong?
Thanks in advance!
Ok, I've found a working solution to my issue after thinking it through step by step. I found I was going about the whole issue entirely the wrong way!
Essentially, I found you don't need any local notifications when the app is active (i.e. in foreground). So instead of setting the local notification when the timer is started, I set the relevant notifications up when the app is about to resign.
I put these observers in my viewDidLoad;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(appWillResign) name:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(appIsActive) name:UIApplicationDidBecomeActiveNotification
object:nil];
and in my appWillResign method, I set up the notifications for any active timers. When the app is resumed, I simply cancel ALL the notifications.
[[UIApplication sharedApplication] cancelAllLocalNotifications];
In short, you shouldn't need to reference the notifications elsewhere in the code. You only set them up when they are absolutely needed: when the app is backgrounded! So you really don't need to 'store' them anywhere for later use.
Thanks for your contribution #Learner, you helped put me on the right track! :D
It is difficult to predict but generally SIGABRT comes when you access an nil or released object.
So in [[UIApplication sharedApplication] cancelLocalNotification:myNotification]; `
it can be "myNotification" which is nil.
OR if [myDefaults objectForKey:#"myNotification"] returns nil.
Possible resason for that can be that NsUserDefaults stores only few data type as mentioned in following paragraph of class reference.
A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
What i can predict is that myNotification which is being stored is of type UILocalNotification. so probably storing of data is failing.
Please debug the code further and post if above cases are not working.
The fix is to sync the NSUserDefaults. I'm assuming you just set the object and did not synchronize it. If so the object will not actually save to NSUserDefaults, rather just to the local NSUserDefaults object. What you need to do is call [defaults synchronize] after setting the object.
We get "CoreData could not fulfill a fault" every once in a while. We have read through the Apple documentation but are unclear on what is allowed to be retained. We have been very careful about creating one context per thread, etc. However, one thing our app is doing is we are retaining NSManagedObjects on our UIViewControllers (usually via a NSArray or NSDictionary). I'm guessing what's going on is the object relationships are changing and we are not handling the appropriate notification.
Does anyone have any suggestions on the better design with regards to Core Data? When we get the error, I cannot see that we actually deleted anything from the context to cause the fault. Is it necessary to handle NSManagedObjectContextObjectsDidChangeNotification on our UIViewControllers if they are retaining state? Any suggestions would be appreciated.
You can register for change notifications in Core Data. This will allow you to update your managed objects when they change. See the Core Data Docs for more info. You're going to be interested in 2 methods to register and respond to changes:
[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:(your NSManagedObjectContext)];
The mergeChanges selector (your method) will call the following method to synchronize any changes from other threads. It will look something like this:
- (void)mergeChanges:(NSNotification *)notification{
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
// Merge changes into the default context on the main thread
[context performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}