Search
Rich's Mad Rants
Powered by Squarespace
« Book Giveaway, Round II | Main | Free copy of Creating iOS 5 Apps, Develop and Design »
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.

References (4)

References allow you to track sources for this article, as well as articles that were written in response to this article.
  • Response
    Freelance Mad Science Labs - Blog - Syncing multiple Core Data documents using iCloud
  • Response
    Response: gigzon.com
    blog topic
  • Response
    Response: food processors
    Freelance Mad Science Labs - Blog - Syncing multiple Core Data documents using iCloud
  • Response
    Response: chinese recipes
    Freelance Mad Science Labs - Blog - Syncing multiple Core Data documents using iCloud

Reader Comments (22)

"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."

iCloud delivers metadata about new documents before it delivers the actual document content. So the second device will be notified that a new document has been created, but you may have to wait until the document content has been downloaded before opening it.

There is some way to query the sync status of an iCloud document, but I don't know it off the top of my head.

January 9, 2012 | Unregistered CommenterBill

Bill,

UIManagedDocument should automatically download the document if it isn't already on the device. So we shouldn't have to wait to open the document. It looks like it creates an empty Core Data document, then downloads the transaction logs. Once it has those, it sends an update notification. Of course, sometimes iCloud takes a very long time to download the transaction logs, and in many applications you won't want the user to start modifying the document until everything has downloaded successfully.

The API does give us the ability to manually download the documents, and to monitor the document state--it might be interesting to use those to see exactly what's going on. But, as I understand it, they shouldn't be necessary.

Actually, iCloud shouldn't ever download the actual files until we request them. iCloud pushes metadata to all registered devices, but the files are only sent upon request (to save bandwidth). So, there shouldn't be any difference between opening the file immediately and waiting. However, a Core Data document is really a series of transaction logs. Does each log file get its own metadata? If that's the case, maybe I'm trying to open the file before it has all the metadata. That might be worth looking into.

I still think the main problem is a race condition. Sometimes we get an update notification before the new data is actually ready. I haven't yet found a good solution for this problem. Though, for many applications, it may not be a real problem, since you probably won't be using two instances of the application simultaneously.

January 10, 2012 | Registered CommenterRichard Warren

Thanks for publishing this info. Very helpful!

It seems like the document name in the iCloud storage settings always show up as the very displeasing "DocumentMetadata". Wondering if you found a way around this?

I tried setting the NSMetadataItemDisplayNameKey when creating the doc using the persistentStoreOptions, but this didn't work.

Have read most of the Apple docs several times. Ugggh.

February 5, 2012 | Unregistered CommenterDaniel

Sorry, Richard. I missed your follow up response until now.

I'm hoping that Core Data/UIManagedDocument/iCloud integration improves with future releases.

I've read that iCloud is supposed to manage the syncing in an atomic fashion if your document is actually more of a file package. That is, you'll be notified of changes to a file package in iCloud after all of its changes have actually been transmitted.

The way that Core Data works with iCloud (not syncing the SQLite data file, generating separate transaction logs, producing externally stored binary data) essentially turns your Core Data-based document into a file package. It's all supposed to work, but I'm not sure that it actually does in iOS 5.0.

The strategy I'm taking with my own Core Data/UIManagedDocument-based app is to avoid the built-in iCloud syncing. As changes are made to records in my app, I plan to write out the records as individual JSON files into a separate/dedicated directory. iCloud can sync individual files without issue. My app will monitor this "export/import" directory for changes and handle the new/changed/deleted external files as appropriate.

So iCloud gets to manage the syncing of my external "import/export" directory. And I'll explicitly manage the import/export of changes from there into my sqilite db. This approach will let me also support syncing via Dropbox as an alternative/backup to iCloud.

UIManagedDocument is too much of a black box for me right now with respect to iCloud. I like using it to locally manage the Core Data stack and external binary data storage. I'm not convinced I want to deal with the vagaries of using it with iCloud. Maybe in iOS 5.1.

Bill

February 5, 2012 | Unregistered CommenterBill

Daniel,

If you save the document inside a Documents folder inside the iCloud Storage container, then the file's name will appear. If you save it inside another folder (or directly inside the container itself), then you should have a single entry "Documents & Data".

If you're seeing "DocumentMetadata", then it sounds like you're storing the core data info in the container's documents folder. I highly recommend not placing Core Data files inside the container's Documents directory, since that will expose all the individual transaction logs. This can cause severe problems, since it will allow the user to delete individual transaction logs, which could put the app in a bad state.

Does that help?

February 6, 2012 | Registered CommenterRichard Warren

Bill,

According to the WWDC slides, it's supposed to break up the data and only sync the changes. I'm not sure, however, that is completely accurate. If you're using an NSFileWrapper, then I imagine that it only syncs the parts of the package that have changed. So, if you're saving your data in small, individual files within the package, it should work.

If you're using Core Data, then it only syncs the transaction logs--which should be even more fine grained. I don't really know how SQLite 3 manages their transaction logs, but I suspect that it's on a per-record basis. If that is the case, then we would be able to upload/download changes on a per-record basis as well. Of course, the actual efficiency can vary greatly depending on how we lay out the data. I image if we put all our data into a single, huge record within Core Data, we may end up transferring the entire record with each transaction log.

Also, if you're saving external binaries, you need to manage those files yourself. iCloud should sync the external files back and forth (as long as their saved in the iCloud container), but you may have to do manual work to synchronize the changes with your Core Data stack. For example, what happens if iCloud syncs a transaction log containing a reference to an external binary before syncing the binary itself. Now, I haven't actually played around with external binaries--so I don't know if there are any problems. But, I can see where there may be additional issues that need to be resolved.

If you're syncing individual files, I suspect iCloud might actually sync the entire file back and forth. Maybe it's just syncing deltas in the background--I'm not sure. It sure looks like we're getting entire files. When we do conflict resolution, we have access to each of the conflicting versions of that file. So, if deltas are used, they system must use them to create each version of the file, and (as far as I can tell) we never get access to the deltas.

As far as UIManagedDocument being a black box--that may be something that we just need to get used to. One of the goals of Object Oriented program is to convert things into black boxes, so that we don't need to concern ourselves with the inner workings. More importantly, I don't expect Apple to become more open and revealing about exactly what they're doing in the background.

However, if you sync your apps with your desktop machine, you can get access to the content of your iCloud containers at ~/Library/Mobile Documents/. You can also access the contents live within your app using any of the standard NSFileManager methods (though, you do need to synchronize reads and writes using an NSFileCoordinator, or you may get bad information or put things in a bad state). I'd recommend poking around a bit, just to give yourself a better sense about what is actually going on.

February 6, 2012 | Registered CommenterRichard Warren

Richard, I do not have a DocumentMetadata.plist, but set NSPersistentStoreUbiquitousContentNameKey and NSPersistentStoreUbiquitousContentURLKey. How can I get it created?

I also deleted the Documents path and the complete data path in iCloud and created the Documents dir new.
But the same, no DocumentMetadata.plist. Please see also my post https://devforums.apple.com/message/620001#620001

February 19, 2012 | Unregistered CommenterDiethard

Diethard,

If you instantiate a new UIManagedDocument, set the NSPersistentStoreUbiquitousContentNameKey and NSPersistentStoreUbiquitousContentURLKey, then save the UIManagedDocument, the system should automatically create a directory using the content name key at the URL provided by the content URL key. It will then create the DocumentMetadata.plist inside that directory.

There are a couple ways to check. First, make sure the data is appearing in your iCloud storage. You can check this from the iOS device's Settings app: iCloud > Storage and Backup > Manage Storage. It should show up as "Unknown."

Next, sync the device back to your development mac. The iCloud containers will be stored in ~/Library/Mobile Documents. You can easily explore the container there and see what, exactly, has been saved. But, looking at my own computer, I'm not sure the DocumentsMetadata.plist actually gets transferred. It looks like it's only the .cdt files were backed up--which I think are the actual transaction logs.

You could change your container URL to the Documents folder inside your ubiquitous container. This will cause all the individual files to show up in the Settings app (including the DoucmentsMetadata.plist). You can also use the standard NSFileManager methods to examine the contents of your content directory in your app's code (thought that's not necessarily easy).

If you're still having trouble, make sure the UIManagedDocument is saving properly. Check the success parameter in saveToURL:forSaveOperation:completionHandler:'s completion handler. You might also want to double check and make sure the keys are correct.

I hope that helps.

-Rich-

February 23, 2012 | Registered CommenterRichard Warren

Richard,

thanks for this info. My documents are stored in the cloud. I also see them if I open the iCloud settings on my Mac. While doing some experiments, I deleted and recreated the iCloud Documents folder some times. One time after creating it again I saw the DocumentsMetadata.plist, but now I can do what I want, the plist is never created. (I check this with [self.managedDocumentQuery setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]]; NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemFSNameKey, @"DocumentMetadata.plist"];)
Possible I created the Documents folder the wrong way? I created a simple directory. Is there a way to cleanup the whole iCloud content for an appID?

The other thing is how to handle the transaction logs: I use NSPersistentStoreUbiquitousContentURLKey and can see files like

file://localhost/private/var/mobile/Library/Mobile%20Documents/<teamId>~com~<comanyName>~/TransactionLogs/.baseline/Test/oobifqo3sgsB_1dHfJvKn595vz3MyE28oJtT0WTNIEg=/baseline.zip
2012-02-17 15:59:17.763 UNOIT[6220:707]
file://localhost/private/var/mobile/Library/Mobile%20Documents//<teamId>~com~<comanyName>~/TransactionLogs/mobile.E6BA544D-06FA-52CB-8B6B-1376B242CF99/Test/oobifqo3sgsB_1dHfJvKn595vz3MyE28oJtT0WTNIEg=/E09E5114-2F18-4165-BBA2-192C5C4A0BA5.1.cdt
file://localhost/private/var/mobile/Library/Mobile%20Documents//<teamId>~com~<comanyName>~/TransactionLogs/mobile.E6BA544D-06FA-52CB-8B6B-1376B242CF99/Test/oobifqo3sgsB_1dHfJvKn595vz3MyE28oJtT0WTNIEg=/B9001A46-57BC-4275-AB75-06BF9A2DAB31.1.cdt

The transaction log path is not <NSPersistentStoreUbiquitousContentURLKey>/< NSPersistentStoreUbiquitousContentNameKey> like written in Apple documents. So if I delete a document I delete the file://localhost/private/var/mobile/Library/Mobile%20Documents//<teamId>~com~<comanyName>~/TransactionLogs directory coordinated. Is this ok and enough?

And another question is how to rename a document inside the cloud. NSPersistentStoreUbiquitousContentNameKey == the name of the document (named from user). I use moveItemAtURL coordinated, but what to do with the transaction logs of the old name?

A lot of questions, but I hope you can help me and can probably write about in your book, also.

Thanks,

Diethard

March 4, 2012 | Unregistered CommenterDiethard

Hmm. The DocumentsMetadata.plist should definitely be there. If it's not showing up, you may have your iCloud container in a bad state (that seems to be easy to do during development). You might need to delete everything and start again. Delete everything from iCloud storage (from Settings or System Preferences on OS X). Delete the app from the test device or simulator. Then, in code delete the entire container. I usually use code like the following:

NSFileManager* fileManager = [NSFileManager defaultManager];
NSURL* ubiquitousURL = [fileManager URLForUbiquityContainerIdentifier:nil];
NSFileCoordinator* coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];

[coordinator
coordinateWritingItemAtURL:ubiquitousURL
options:NSFileCoordinatorWritingForDeleting
error:nil
byAccessor:^(NSURL *newURL) {
[fileManager removeItemAtURL:newURL error:nil];
}];

Use the removeItemAtURL:error: method with the URL returned by URLForUbiquityContainerIdentifier:. The only catch is that this must be done in a file coordinator block.

As far as transaction logs go, the general rule is, if you delete the UIManagedDocument, you must also delete the transaction logs. If you only have a single managed document, then you can just clear the whole container (as shown above). Otherwise you must get the URL for that document, and clear that entire directory.

Typically, I search for the DocumentMetadata.plist and then delete its parent directory--since we cannot search for directory names directly.

As far as renaming the documents, I'm not exactly sure how to go about that. UIManagedDocument saves all its data in a directory. So, we'd need to move that entire directory. We'd also need to change the NSPersistentStoreUbiquitousContentNameKey. But, there may be some internal data that also needs to be modified. My guess is that I'd use saveToURL:forSaveOperation:completionHandler: to create a new file with the new name, then delete the old one.

However, this may not be required. After all, we're not supposed to show file-level details to the users in iOS. So your documents could have a display name that is simply saved as a string somewhere in the document's data. The user could then change the display name without ever touching the NSPersistentStoreUbiquitousContentNameKey.

I hope that helps.

-Rich-

March 7, 2012 | Registered CommenterRichard Warren

Hi Richard,

thank you very much for these details. I now can read the DocumentMetadata.plist:

- (NSString *)ubiquitousContentName {
__block NSString *retval = nil;
NSURL *metadataURL = [_document.fileURL URLByAppendingPathComponent:@"DocumentMetadata.plist"];
if (metadataURL) {
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
NSError *error = nil;

[coordinator coordinateReadingItemAtURL:metadataURL options:0 error:&error byAccessor:^(NSURL *newURL) {
NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfURL:newURL];
retval = [dict objectForKey:NSPersistentStoreUbiquitousContentNameKey];
}];
}

return retval;
}

My mistake was to perform

[self.managedDocumentQuery setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K like %@", NSMetadataItemFSNameKey, @"DocumentMetadata.plist"];

of course DocumentMetadata.plist is not a file under /Documents but a file inside the file wrapper /Documents/<myDocument>. I think the query above will search under /Documents

Ok, but if I delete a document and try to delete coordinated the transaction logs at <transactionLogURL>/<ubiquitousContentName> I get a The operation couldn’t be completed. (Cocoa error 4.). If I browse through the transactionLogURL with [dirEnum nextObject] I see that there are files like

.baseline/current.nosync/<ubiquitousContentName>/
mobile.E6BA544D-.../<ubiquitousContentName>/...
.cdmetadata/metadata.nosync/mobile.E6BA544D.../<ubiquitousContentName>

So there are subdirs between <transactionLogURL>/<ubiquitousContentName> and I´m not shure how to delete the transaction logs.

March 9, 2012 | Unregistered CommenterDiethard

Hmm...Here are a few suggestions.

First, I'd never put Core Data documents in the iCloud Documents folder. This will expose all the individual files to the user. That means all the transaction logs, the metadata plist, and all kinds of junk that the users really shouldn't be aware of. Worse yet, this lets users manually delete individual files from within a document--which will undoubtedly put the document into an invalid state.

If you want to give the user the ability to delete individual documents, add that functionality to the application itself. But don't let them mess with the individual files from within the iCloud settings.

Second, it looks like you're storing and reusing the transactionLogURL. You should never store iCloud URLS. You should always get the url either from calling NSMetadataQuery (for existing files) or create it by calling URLForUbiquityContainerIdentifier: and appending the necessary directory or file names (for new files).

For example, if I call URLFor Ubiquity... to get the container URL, then append my file name to make a suitable document URL, I can then use this URL to create my document--however iCloud reserves the right to change this URL, so I cannot hold onto and reuse this URL.

Of course, if you have an open document, you can access its URL using document.fileURL--but I'm not sure why you would ever need to do this. I only search for the metadata plist file so that I can calculate the document's URL (by calling URLByDeletingLastPathComponent to get the URL for the containing directory). In my sample code, I also use the NSPersistentStoreUbiquitousContentNameKey as the file's display name--which requires reading and parsing the metadata plist. However, that may not be the best approach. Saving the file's display name inside core data itself may provide greater flexibility--especially when it comes to "renaming" files. You could just change the display name inside Core Data and leave the file system untouched.

The only exception is when deleting an open file. In that case, I'd close the file, then delete the entire directory at document.fileURL. I still wouldn't go rooting around inside those directories.

Finally, I haven't had any problems using NSMetadataQuery with the following code:


NSMetadataQuery* query =
[[NSMetadataQuery alloc] init];

[query setSearchScopes:
[NSArray arrayWithObject:NSMetadataQueryUbiquitousDataScope]];

// We cannot look for the folder--must look for the
// contained DocumentMetadata.plist.
[query setPredicate:[NSPredicate predicateWithFormat:@"%K like %@",
NSMetadataItemFSNameKey,
@"DocumentMetadata.plist"]];

It finds the DocumentMetadata.plist file, even though it's buried in a sub directory.

-Rich-

March 10, 2012 | Registered CommenterRichard Warren

Richard,

I think I must explain in more detail what I´m doing:

I used the Documents folder in iCloud as described in the Stanford CS193p lesson:
"Assuming you just have one container in your Entitlements ...
[[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
Usually you put your documents in the @“Documents” directory (special) inside this URL."

My iCloudCoreDataLogFilesURL is [[self iCloudURL] URLByAppendingPathComponent:kCoreDataLog];
where [self iCloudURL] returns [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

I have a tableView with multiple documents. The user can tap the add button and enter a name for a new document. Then a UIManagedDocument is created with UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:docUrl];
where docURL is [[[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil] URLByAppendingPathComponent:@"Documents"] URLByAppendingPathComponent:title];

If the document already exists, I add a number at the end.
Then I set the options:

- (void) setPersistentStoreOptionsInDocument:(UIManagedDocument *)document
{
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];

if (self.useiCloud) {
NSString *name = [[document.fileURL lastPathComponent] stringByDeletingPathExtension];
[options setObject:name forKey:NSPersistentStoreUbiquitousContentNameKey];

NSURL *logsURL = [self iCloudCoreDataLogFilesURL];
[options setObject:logsURL forKey:NSPersistentStoreUbiquitousContentURLKey];
}

document.persistentStoreOptions = options;
}

So the logsURL is the same for all documents.

Then saveToURL is called with ubiquitous docUrl described above, saving the document directly in iCloud.

When the user taps on a document, I have the UIManagedDocument (tableView datasource) and have it´s fileURL. Then the DocumentMetadata.plist is read:

NSURL *metadataURL = [_document.fileURL URLByAppendingPathComponent:@"DocumentMetadata.plist"];
if (metadataURL) {
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
NSError *error = nil;

[coordinator coordinateReadingItemAtURL:metadataURL options:0 error:&error byAccessor:^(NSURL *newURL) {
NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfURL:newURL];
NSLog(@"dict: %@", dict);
retval = [dict objectForKey:NSPersistentStoreUbiquitousContentNameKey];
}];
}

The dictionary only contains the contentName, not the logURL??

So I construct the logURL again with [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil] URLByAppendingPathComponent:kCoreDataLog];

and set the options containing this logURL and the contentName from DocumentMetadata.plist.

This works fine, but again I do not know how to delete the transactionLog if deleting a document.

In your example you use NSMetadataQuery to get DocumentMetadata.plist. But you only have one document. I have multiple documents and each of them has it´s own DocumentMetadata.plist.

Use an NSMetadataQuery object to search for documents in iCloud.
Metadata queries identify all of your Core Data documents, regardless of whether they were created locally or on another device. For documents created on other devices, the only thing present in the document’s file package initially is the DocumentMetadata.plist file.

March 11, 2012 | Unregistered CommenterDiethard

Diethard, I had a family emergency over the last few days, and I've been away from my computer. I had written a response, but after thinking about it a bit, there are a few things I'd like to test out.

However, I would like to say that I'm still a bit unsure on how you're going about this. It seems to me you should start with an NSMetadataQuery to get the list of all the files. Then you should use that list to populate the table view of documents. Finally, once the user selects a document, open it by instantiating a UIManagedDocument and loading the proper URL.

I actually have two samples that may be relevant. The first is Chapter 7 of my book. That covers the basics on using UIManagedDocument, but does so with a single-document (library-style) app.

The second sample is the original post that these comments are attached to. If you look at that source code, you will see that I am doing something very similar to what you want to do--I am letting the user create new documents and displaying the list of documents in a table view. However, I don't deal with the issue of deleting documents in this sample.

And that's what I want to check. I think, if you use the same URL for the file and the transaction logs, then the transaction logs will be placed in a subfolder inside the document's folder. Therefore, deleting the document folder will clear everything. However, I want to double check this. I'm not sure how to best delete the document if you're saving it in a separate folder.

March 13, 2012 | Registered CommenterRichard Warren

Thank you very much Richard. I´m on vacation for some days ...
I also started an Apple DTS request and already got answer. I will test this after I´m back.

March 15, 2012 | Unregistered CommenterDiethard

@Richard

Thanks for the advice about storing UIManagedDocs outside of the Documents directory. Makes sense. My previous response got lost somehow.

Anyway, I was wondering if you ever got past your main issue in this article? That is, over time, syncing seems to degrade and the import notifications stop arriving or are incomplete. You had identified that adding a delay in responding to the import notification had some value, but eventually proved to be unreliable.

I've been churning along with my little app using UIManagedDocument with iCloud, avoiding some of the bigger issues like allowing users to switch iCloud on and off, but gradually ramping up the complexity of my data model - and hoping that one day it will magically become 100% reliable. Alas, that day has not yet arrived, although iOS 5.1 seemed to resolve a couple bigger issues - like dealing with deleting the same record on two different devices.

As my app starts to look more real, I'm starting to take a harder look at this reliability issue. I've found, as you did, that I can often move out of a stalled sync state by force quitting the app. This makes me think the iCloud infrastructure is working more reliably, but that my app is somehow missing the magic ingredient to keep the syncs working. I guess I half expected that as my data model got more complex that the whole database sync technology would start cracking. However, I've been pleasantly surprised with how well it can resolve differences across the various CRUD operations, including multiple parent-child entity relationships.

I don't currently have the proper logging in place to help isolate the issue. That will be my next step.

In any case, thanks again for posting this great article and sample code. It really helped a lot. It's embarrassing that Apple hasn't been more forthcoming with cloud-based UIManagedDocument samples (unless I've missed them).

March 21, 2012 | Unregistered CommenterDaniel

Daniel,

I haven't had a chance to really test iOS 5.1 yet. It's been on my ToDo: list since it was released. Hopefully I can get to it sometime this week and report on the results.

The problem I had seemed to be caused by a race condition. Sometimes I would get the notification that my persistent store had been updated from iCloud--but the updated information would not be available yet. This seemed to happen about 1/4 of the time without the delay, and about 1/12th of the time with the delay.

It wasn't like the stability degraded...the system would always catch the update the next time I launched the app, and the auto conflict resolution fixed the problem. And then it would continue to function normally. But, eventually, it would drop another update.

In part, I wonder if we (as developers) simply have unrealistic ideas on how iCloud syncing should perform. I mean, I want to see it working in my tests. And I want to assure myself that it works 100% of the time. But, I also have to admit, the way I test my apps is somewhat artificial. Most people won't be running it on two devices simultaneously, ping-ponging updates back and forth between them. Maybe iCloud works fine, but just works on a much longer time frame.

After all, even if iCloud syncing was 100% rock solid, I'm not sure we would still have completely stable syncing. Any number of problems (from network connectivity to app crashes/device shutdowns) could cause syncing problems. Imagine that I pull out my phone, make a few edits, then put it to sleep and shove it back in my pocket. Will my changes be saved? I'm not sure. I assume my app pauses while the phone is asleep. Since I'm saving on a background thread, even if I call save immediately after the change is made, there's a good chance the data won't actually be saved before the device goes to sleep. Does the iCloud service upload/download changes while the device is asleep? I honestly don't know. But either of these could cause an apparent "failure" in my application's syncing. I get home, pop open my iPad, and my data's not there. But it's not really iCloud's fault.

At some level, I think we just need to trust that iCloud will eventually push out the information, and our conflict resolution code (or core data's automatic conflict resolution) will fix any problems that arise.

Still, I hope Apple fixes the race condition bug. That just shouldn't happen.

March 21, 2012 | Registered CommenterRichard Warren

Hi Richard,

I now know what I did wrong: I forgot to append the value of <NSPersistentStoreUbiquitousContentNameKey> to the <transactionLog> url. I create now the complete transaction log url:

- (NSURL *) iCloudCoreDataLogFilesURL:(NSString *)contentName {
return [[[self ubiquitousURL] URLByAppendingPathComponent:@"transactionLog"] URLByAppendingPathComponent:contentName];
}

With this it is easy to identify the transaction logs of a specific document.

March 31, 2012 | Unregistered CommenterDiethard

Oh great. I'm glad you figured it out. Sorry I wasn't able to get back to this problem earlier. I just barely managed to test out UIManagedDocument and iOS 5.1.

-Rich-

March 31, 2012 | Registered CommenterRichard Warren

Hi Richard,

thanks for your help! My first universal app with iCloud support is now available in the app store: http://itunes.apple.com/us/app/calquencer-calendar-sequencer/id517989829?mt=8

Calquencer a small, tiny app for creating sequences of iOS calendar events. The user can create event templates with title, location, alarms, etc. and add to each template a sequence of dates with individual notes. A tap on the action button and for each sequence date an event in the iOS event database is created. This app is very useful for events where it is not possible to define a recurrence rule. The event templates are stored locally or in iCloud.

May 13, 2012 | Unregistered CommenterDiethard

Hi Richard,

In iCLOUD LIMITATIONS page 396, bullet point 3 you stated:

When creating a new store, we should not populate it with a pre-existing database file. If we need to set up some initial data, we should either programmatically create the data in code or use NSPersistentStoreCoordinator’s migratePersistentStore:toURL:options:withType:error: to load the data from an existing file.

I seen this in the Apple docs also but I can not find an example (code) of it's use.
Would you post a sample code for the use of ;
--------------------
migratePersistentStore:toURL:options:withType:error:
(Moves a persistent store to a new location, changing the storage type if necessary.)

- (NSPersistentStore *)migratePersistentStore:(NSPersistentStore *)store toURL:(NSURL *)URL options:(NSDictionary *)options withType:(NSString *)storeType error:(NSError **)error

-------------------------
Me and many other would be very thankful.

Lar

June 19, 2012 | Unregistered CommenterLar

Lar,

I'd like to write something up, but I'm not sure when (or if) I'll be able to get around to it. In the meantime, I'd recommend checking out the WWDC 2012 videos. There's a session on iCloud and Core Data that describes preloading data into an iCloud persistent store.

I hope that helps,

-Rich-

June 27, 2012 | Registered CommenterRichard Warren

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>