Sync or Swim

Previously: An introduction to my series of blog posts on iCloud document syncing, and issues with provisioning and entitlements.

Part 2: The Documentation for Documentary

When last we left our heroine, she had configured her ADC accounts properly to allow her to run the Documentary iOS and Mac sample apps. (Not a very exciting afternoon matinée, I’ll admit.)

Great, so they run, but what do they do?

The short answer is: in order to share documents between iOS and Mac, they do what the documentation says to do.

Shared Ubiquity Containers

Apple’s produced some good iCloud documentation (PDF), so go read that first if you’re interested (remember, this is not a full tutorial).

The first thing from the documentation that’s important to our specific project is that we want to share a ubiquity container for both of our apps, Documentary for iOS and Documentary for Mac. Normally, you’d use the unique, separate bundle IDs of your apps as their primary ubiquity container identifiers, but here we’re going to use the iOS bundle ID for both, so all their contents are shared. This is not a hack: Apple recommends it for precisely this case.

Just like for your app ID, this ubiquity container ID is actually prepended with your team ID. So make sure you use the same team for your iOS and Mac projects (this should be the default). Normally, therefore, you’d need to hard-code your team ID into your code to retrieve the URL for this ubiquity container. But Apple makes it easy by returning the first container in your app if you pass in nil:

    [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

In our case, we only have one ubiquity container, so nil it is!

I was a little surprised to read that you can put your own folders inside the ubiquity container, really whatever you want to do there, and it will be replicated for you. It’s not just one level deep. The Documents folder is special because its contents are user-visible on the Mac side (for a certain value of “user visible”, keep reading), but other than that, anything goes, as long as you keep in mind that your free space is limited by your iCloud account’s storage quota.

Turning iCloud On and Off

The second thing that struck me in the documentation is all the work you need to do to handle turning on and off iCloud support. You’re supposed to call this method in your application:didLaunch... code:

    [[NSFileManager defaultManager] ubiquityIdentityToken]

and if it returns a non-nil value, then the user is signed in to iCloud and your app can use it…well, maybe you can use it. You should also call the above-mentioned URLForUbiquityContainerIdentifier:—on a background thread, mind—before doing anything. In theory, a non-nil result from the former should be enough to assume iCloud availability. But what if the first returns non-nil but the second returns nil?

In any case, if iCloud is available but your app isn’t using it, you should ask the user whether they want to use it, and cache that choice so you don’t ask it again the next time. If you were using it, but now it’s off, you should…tell the user they just lost all their data? “Sorry, fuck off”? The documentation isn’t forthcoming. Wenderlich’s tutorial makes it sound like you can still access the local cache of the iCloud files, despite iCloud being off. Even if that’s true, presumably anything that was “in the cloud” and not downloaded can’t now be downloaded after you’ve logged out. Same if you’ve switched accounts.

But if the user agrees to start using iCloud, you’re supposed to copy all your local documents to the ubiquity container, where they will then be synced with iCloud. This is entirely on you, the developer. If there are naming conflicts, you need to resolve them in your own custom code. This is the first of many places where I wish Apple had done more to help us out. Making each app do this individually means that all apps will do it inconsistently, and many will miss the edge cases that don’t show up in casual testing.

(The Documentary project punt on this entirely by assuming iCloud is always on, sorry.)

Mac Versus iOS File Philosophies

I show the documents on both Mac and iOS in my own list UI, which is…frowned upon in OS X. The documentation makes clear, both in explicit and implied ways, that on the Mac they really want you to rely on the user employing the standard Open dialog to find files, and they want all file interaction to go through NSDocument, which handles downloading and uploading file content, file version conflicts, and files being moved or modified elsewhere.

On iOS, there is no such functionality, so you must find the files yourself using NSMetadataQuery. The iCloud document makes mention of NSMetadataQuery but doesn’t explain how to use it in any way. It just refers to a second document, File Metadata Search Programming Guide (PDF). This is the danger of using what my Edge Cases cohost refers to as “collaborating objects”, where all sorts of framework objects tie together to make a more capable whole; learning to do even the simplest things from scratch means hopscotching from one long document to another, never quite learning the exact steps you need. Frustration with that approach led to my finding Ray Wenderlich’s tutorial, where he had a very helpful example of NSMetadataQuery usage that I copied wholesale.

The long and short of it is, if you keep the query going, you’ll be notified when there are changes, and you can update your list to match. I do this on iOS, but I also do this in the Mac app, because even though it’s not recommended, the functionality is in Foundation and shared between both platforms.

But to manipulate files directly, instead of through a UI/NSDocument class, requires using a new class, NSFileCoordinator. It’s interesting how little change was made to low-level systems to get this to work; instead, new functionality and restrictions were piled on top, just like NSFileCoordinator was piled on top of preexisting NSFileManager methods. One doesn’t replace the other, both are still needed.

Also keep in mind that unlike on iOS, all your documents are fully reachable by the user on Mac, if they know how to look. The way to look is to do a Spotlight search for a specifically-named document, which will find the iCloud Documents folder for your app. It won’t be labeled anything specific, just iCloud. But once you’re there, you can delete files, move files, rename files, and even drag them into different apps to edit them there, all without your app being able to do anything about it. Is this a feature? A bug Apple just didn’t get around to fixing? Dunno.

Further Mac Versus iOS Difficulties

The documentation mentions, but has no overarching solution for, the fact that many of what you would consider to be foundation-level classes are actually quite different on Mac vs. iOS. For one, think about colors: NSColor (Mac) and UIColor (iOS) are not transferable.

For two, the coordinate systems is different.

For three, the file system is different, or probably so. On iOS, the file system is case-sensitive, and you can’t change that. On the Mac, by default it’s case-insensitive, and while a diehard Unix neckbeard might change that, most users will not, so it’s a scenario you have to be ready to deal with.

The docs blithely say, “To make your document file format cross-platform compatible, you must read and write files in a case-insensitive manner.”

Sure, sure, is that all? Remember, I spent a whole episode of Edge Cases talking about how complicated and crazy Unicode is. What does case even mean for non-ASCII, non-Western characters? I couldn’t tell you.

And even if you set that aside, let’s say you want to create a new file with a certain name, “Document 1.txt”. How do you tell, on iOS, that there’s no file in that location already with any case-only variant of that name, such as “document 1.txt” or “Document 1.TXT”?

Well, you could iterate through every file in the directory, and compare its name in a case-insensitive fashion with your new name string. (Hard if you’ve got thousands of files.) Or you could keep your own dictionary of actual names mapped to, say, uppercase versions of those same strings, and so look up an uppercase version of your new name in a dictionary of them. Either way, it’s extra, fiddly, work, that’s still not guaranteed to be completely full proof.

Remember, Apple has said more than once that you should just go ahead with file operations, instead of trying to preflight them, because the situation could change out from under you between your test and your actual file system work. That’s still true here, and there’s no way to fully compensate for it.

Next up: Some bad (NSMetadataQuery issues), some good (NSDocument conflict resolution), and conclusions.