Everything but the Kitchen Sync

Previously: part 1, provisioning and entitlements and part 2, iCloud syncing documentation.

Part 3: Some Bad, Some Good

When last we left our hero, he had read Apple’s iCloud documentation (PDF) and found it both helpful for the details it included, and frustrating for the difficult edge cases it left as “an exercise for the reader”. (Thus mirroring the opinions of this blog’s author!)

Listing and Syncing

One of those edge cases, surprisingly enough, appears to be just trying to maintain on ongoing list of documents.

My Documentary sample apps, for the sake of simplicity (and, once I started running into problems, a certain sense of orneriness) allow for the creation, deletion, and renaming of documents directly from their list, without requiring individual per-document UI. This in particular allows me to put all this logic in a platform-agnostic shared class, in my case called iCloudManager, instead of duplicating it in UIDocument for iOS and NSDocument for Mac.

Creating a new document involves calling the NSString method writeToURL:atomically:encoding:error: on a blank string instance, and this works, even in a ubiquity container, but it only works if you wrap it in a NSFileCoordinator call:

    NSError *coordinateError = nil;
            
    __block BOOL writeResult = NO;
    __block NSError *writeError = nil;
            
    [fileCoordinator coordinateWritingItemAtURL:newDocumentURL 
options:0 error:&coordinateError byAccessor:^(NSURL *newURL) {
        writeResult = [@"" writeToURL:newURL atomically:YES encoding:NSUTF8StringEncoding error:&writeError];
        NSLog(@"Write %@ success: %ld, error: %@", newDocumentNamePlusExtension, (long)writeResult, writeError);
    }];            

    if (coordinateError == nil && writeResult == YES) {
        succeeded = YES;
    }

The NSFileCoordinator is synchronous, but doesn’t return a boolean, unlike most such Apple APIs—if the NSError is nil, it succeeded…or did it? In fact, it seems to only return coordination errors. If there was an actual file writing error, you can see that there is no way for the custom block to return any value. My code gets around this by saving the results from within the block, and checking that as well. I have not had a chance to check what the UI/NSDocument APIs do; one would hope they would have similar workarounds. The deletion code is similar.

The bad news is, if you’re relying on NSMetadataQuery to maintain your list of files, it’s not going to update your list instantaneously. The documentation for NSMetadataQuery says, “By default, notification of updated results occurs at 1.0 seconds.” I’ve found that even if I set notificationBatchingInterval to a very low value, it still takes about a second. And the update, when it occurs, won’t say anything as helpful as, “Yes, we deleted a file.” Instead, it will just tell you there’s been some sort of change.

So, for example, if you wanted to highlight the row of the newly-created file, you need to wait for the next update, then try to find that URL in the latest results list, and select it then. That should work, but it’s awfully fiddly. What if a network update comes through right then? Should you keep your conditional code running for the next several updates, till you find the URL you want? Deletion and renaming updates are similarly delayed and contextless.

For Documentary, I just punted and only updated the UI once the NSMetadataQuery results came in. But for a shipping app, I’d have to do a lot more polish work.

Putting the ”Document” in ”Documentary”

But finally, there was no more putting it off, I had to create per-platform Document subclasses if I wanted to implement in-app text editors. Actually, for Mac, I made it so that Cmd-O opened your document via Launch Services, by default with TextEdit. But I also included an “Open Document Internally” option, for reasons I’ll get to below.

And the editors, really just simple text views, worked pretty well! I had to hook up my text views to the undo manager to get iCloud to work properly, but that was described in the documentation and pretty easy. Here’s the change method on iOS:

    - (void)textDidChange:(NSNotification *)notification {
        // In a real app, we would only register a change after a certain amount of typing, or after a certain time. But not for this sample app.
    
        [[self.document.undoManager prepareWithInvocationTarget:self.document] setText:self.document.text];
        [self.document.undoManager setActionName:NSLocalizedString(@"Typing", @"Undo/redo label")];
    
        self.document.text = self.textView.text;
    }

I really should have coalesced the text changes, but didn’t have time, sorry.

And while I punted on conflict resolution on the iOS side due to time issues (again, sorry), I knew that on the Mac, it should all be taken care of if I used NSDocument.

So…was it?

I’m happy to report that it was. When I opened a document that had been modified locally and on two other devices, the document window presented a sheet with the three options, clearly labeling where they came from, and showing you a preview of their contents, all without my having to do any extra work. You could even choose to keep several of the conflicting versions around. Neat! Note: it won’t do any of that work for you until you open a document, but it’s very nice to have it then.

Conclusions

I spent about a week of free time putting all this together, and I have a much better sense of the contours of iCloud document syncing than I used to. The good: the syncing itself, Xcode 5, the documentation, and NSDocument. The bad: edge cases and lots of developer-required logic.

To play around with it, feel free to clone and build my Documentary applications, but keep in mind I did this in a week, and that you’d have to add a lot of extra error-handling and edge case-handling code before you’d want to ship anything based on it.

Still, I had fun, and I hope you had fun reading about it, too.