Search
Rich's Mad Rants
Powered by Squarespace

Entries in UIManagedDocument (1)

Monday
Dec192011

Syncing multiple Core Data documents using iCloud

In "Creating iOS 5 Apps", chapter 7 demonstrates how to sync a single-document Core Data application using iCloud. As the name suggests, single-document apps (also called library apps) use a single Core Data persistent store to manage all of the applications data. However, the question came up, how can you modify this for a multi-document application--an application that used a separate Core Data persistent store for each document. I have some sample code that seems to (mostly) work. It needs some heavier testing, and there are some wrinkles that need to be worked out. So take a look and let me know if you have any suggestions.

Here are the key changes:

In the single-document app, we always knew that we had one and only one copy of the persistent store. This let us greatly simplify the design. If the persistent store didn't exist, we created a new copy in our application's sandbox. If iCloud is enabled, we set the NSPersistentStoreUbiquitousContentNameKey and NSPersistentStoreUbiquitousContentURLKey in the store's options, and we're good to go.

In the multi-doc app, we don't know if we have any documents, or how many documents we may have. This means we must search for our documents using NSMetadataQuery. I've shown using NSMetadataQuery in Chapter 6, but this time there are two important differences. First, we want to continue to look for new files after the search's initial gathering phase completes. This means we leave the Query running. We simply disable it when we want to iterate over the results, then re-enable it.

Second, we cannot search for the file name given to UIManagedDocument, since this creates a directory with the given name. Instead, we must search for a file called DocumentMetadata.plist. The directory containing that file will be the URL we use to create our UIManagedDocument. The value from DocumentMetadata.plist's NSPersistentStoreUbiquitousContentNameKey will be the value we use for the UIManagedDocument's persistentStoreOption key.

This also means we cannot simply create local copies of our UIManagedDocument anymore. We must move the document into the iCloud container--otherwise the DocumentMetadata.plist won't show up in our queries. Note that iCloud still only syncs the transaction logs, and the database itself is automatically marked as .nosync.

Sample Code

I've created sample code that shows how this works. The Core Data model is pretty pathetic. I have a single entity with the text, title and modification date. Each document should never have more than 1 entity. Which, actually, makes this a pretty poor choice for Core Data. Still, it demonstrates the key points.

I'm also using the same URL for the UIManagedDocument and the NSPersistentStoreUbiquitousContentURLKey. This will place both my persistent store and my transaction logs in the same directory. Note that, you could create separate URLs for both (as long as they're both subdirectories inside the iCloud container), but this simplifies the code somewhat. Remember, if you delete the document you also need to delete all the transaction logs.

When you launch the app, you get a list of files. Select a file to open it. You can modify either the title (in the text field) or the text (in the text view below the text field). These changes aren't saved until you exit the file (navigating back to the file list). At that point, the changes are saved to the persistent store, and then synced through iCloud. Tapping the + icon in the file list will create an empty file.

If you run the app on two devices, you can watch the synchronization occur. In the file view, add a new file to one device. It will soon appear on the second device. Open the same file on both devices. Modify the text on one. Close and re-open the file to force a save. The changes should soon appear on the other device (within a few seconds).

Bugs (or features)

First, if you watch the console, you'll sometimes see errors when the app tries to receive updates.

2011-12-18 19:32:33.443 MultiDocument[6584:3f37] +[PFUbiquityTransactionLog loadPlistAtLocation:withError:](324): CoreData: Ubiquity:  Encountered an error trying to open the log file at the location: <PFUbiquityLocation: 0x1ee6c0>: /private/var/mobile/Library/Mobile Documents/WNZF6NN7ZY~com~freelancemadscience~MultiDocument/mobile.D07EBC6E-03EA-5295-8D2F-0C1D2E737524/Test Document 1/WdKZGuhOiADrlyspq5GroEkGHfNbxImTR1BYSTCku1A=/08F64E33-39FC-4AFE-A872-A21C3282CDBD.1.cdt

Error: Error Domain=NSCocoaErrorDomain Code=256 "The operation couldn’t be completed. (Cocoa error 256 - The item failed to download.)" UserInfo=0x119240 {NSURL=file://localhost/private/var/mobile/Library/Mobile%20Documents/WNZF6NN7ZY~com~freelancemadscience~MultiDocument/mobile.D07EBC6E-03EA-5295-8D2F-0C1D2E737524/Test%20Document%201/WdKZGuhOiADrlyspq5GroEkGHfNbxImTR1BYSTCku1A=/08F64E33-39FC-4AFE-A872-A21C3282CDBD.1.cdt, NSDescription=The item failed to download.}

It looks like iCloud will try again, and these seem to resolve themselves (thought they can take a while). Very rarely, things seem to get really bad--to the point where iCloud is almost unusable. Most of the time, however, you can run the app without a hitch. I think it must be a glitch within iCloud, but I can't be sure.

Another bug often shows up when I start trying to pass changes back and forth between devices that both have the same file open. The first sync always seems to work--and it seems to work pretty well as long as each subsequent sync is in the same direction. But, when I go back and forth, eventually one of the devices will receive an update notification, but doesn't receive any new data.   Usually closing and reopening the file successfully updates the data.

I think there may be a race condition somewhere, and the notification arrives before the persistent store is really ready. I've tried delaying my response to the notification by 0.25 seconds, and in the next test, it took almost a dozen syncs before I triggered the bug. So that might help, but it doesn't fix the problem.

I've also tried clearing both the child and parent managed object context, then re-fetching the object, just to make sure I went all the way back to the persistent store, and I still get the old data--not the update. I've reported a bug about this--though I'm not sure if it is really a bug in iCloud or a problem in my code. If anyone has any suggestions, please let me know.

Also, the syncing problems seem much worse if you create a file on one device then open it as soon as it appears on the other. I'm not sure why this is. Perhaps an important file hasn't synced over yet, and things start out in a bad state. Syncing seems much more reliable if I launch one device. Create a document. Modify the document and save it. Then launch the second device, and open the file there. Again, it probably needs additional testing, and any suggestions would be greatly appreciated.