Resolving Document Version Conflicts

In an iCloud world, when a user has installed a document-based application on multiple devices or desktop systems, there can be conflicts between different versions of the same document. Recall that an application updates a document file in the local container directory and those changes are then transmitted—usually immediately—to iCloud. But what if this transmission is not immediate? For example, you edit a document using the Mac OS X version of your application, but you’ve also edited the same document using the iPad version of the application—and you did so while the device was in Airplane Mode. When you switch off Airplane Mode, the local change to the document is transferred to iCloud. iCloud notices a conflict and notifies the application.

Learning About Document Version Conflicts

As “Monitoring Document-State Changes and Handling Errors” describes, your application becomes aware of document-version conflicts by observing the UIDocumentStateChangedNotification notification. If the documentState property changes to UIDocumentStateInConflict, multiple versions of the same document exist. The application is responsible for resolving those conflicts as soon as possible, with or without the user’s help.

You learn about the conflicting versions of a document through two class methods of the NSFileVersion class. The currentVersionOfItemAtURL: method returns an NSFileVersion object representing what’s referred to as the current file; the current file is chosen by iCloud on some basis as the current “conflict winner” and is the same across all devices. By calling the unresolvedConflictVersionsOfItemAtURL: method, you get an array of NSFileVersion objects; these objects are called conflict versions, and each represents an unresolved version conflict for the file located at the specified URL. NSFileVersion objects can give you information helpful in resolving conflicts, such as modification dates, localized document names, and localized names of saving computers.

Strategies for Resolving Document Version Conflicts

Your application can follow one of three strategies for resolving document-version conflicts:

Which strategy is best to use depends a lot upon your document data. If you can merge the contents of different document versions without introducing contradictory elements, then follow that strategy. Or choose the document version with the latest modification date if your application doesn’t suffer any loss of data as a result.

Generally, you should try to resolve the conflict without involving the user, but for some applications that might not be possible. If an application takes the user-centered approach, it should discreetly inform the user about the version conflict and expose a button or other control that initiates the resolution procedure. “An Example: Letting the User Pick the Version” examines the code of an application that lets the user select the document version to use.

How to Tell iOS That a Document Version Conflict Is Resolved

When your application or its users resolve a document version conflict by picking a version of a document, your application should complete the following steps:

An Example: Letting the User Pick the Version

Our sample document-based application is a simple text editor. It would be difficult for such an application to locate and merge textual differences in conflicting versions of the document, and even if it did, the resulting document might not be what the user wants. The application could pick the document version with the most recent modification date, but then again there’s no way to be certain that is the version the user wants. A good conflict-resolution strategy in this case is to let the user, who is most familiar with the document’s contents, pick the version she or he wants.

You might recall the code shown in Listing 6-1 from “Monitoring Document-State Changes and Handling Errors.” This code shows the method of the document’s view controller that handles the UIDocumentStateChangedNotification notification posted by UIDocument when there is a change in document state. If the new document state is UIDocumentStateInConflict, the view controller shows a Resolve Conflicts button in a custom status view. (It also sets the color of the status indicator to red.)

Listing 6-1  Detecting a conflict in document versions

-(void)documentStateChanged {
    UIDocumentState state = _document.documentState;
    [_statusView setDocumentState:state];
    if (state & UIDocumentStateEditingDisabled) {
        [_textView resignFirstResponder];
    }
 
    if (state & UIDocumentStateInConflict) {
        [self showConflictButton];            // <------ Shows "Resolve Conflicts" button
    }
    else {
        [self hideConflictButton];
        [self dismissModalViewControllerAnimated:YES];
    }
}

When the user taps the button, UIKit invokes the method in Listing 6-2. This method displays modally the view of a custom conflict-resolver view controller.

Listing 6-2  Showing the user interface for resolving document version conflicts

-(void)conflictButtonPushed
{
    ConflictResolverViewController* conflictResolver = [[ConflictResolverViewController alloc]
        initWithURL:_document.fileURL delegate:self];
    [self presentViewController:conflictResolver animated:YES completion:nil];
    [conflictResolver release];
}

The ConflictResolverViewController object creates a page view controller (UIPageViewController object) that allows user to page between, and examine, the current-file document and each conflict-version document. In the tool bar of each document view is a Select Version button. If the user taps that button, one of the two custom delegation methods shown in Listing 6-3 is called, depending on whether the chosen document is the current-file document or a conflict-version document.

Listing 6-3  Resolving a document version conflict

-(void)conflictResolver:(ConflictResolverViewController *)conflictResolver
       didResolveWithFileVersion:(NSFileVersion *)fileVersion {
    [self dismissViewControllerAnimated:YES completion:nil];
    [fileVersion replaceItemAtURL:_document.fileURL options:0 error:nil];
    [NSFileVersion removeOtherVersionsOfItemAtURL:_document.fileURL error:nil];
    [_document revertToContentsOfURL:_document.fileURL completionHandler:nil];
    NSArray* conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:_document.fileURL];
    for (NSFileVersion* fileVersion in conflictVersions) {
        fileVersion.resolved = YES;
    }
}
 
-(void)conflictResolverDidResolveWithCurrentVersion:(ConflictResolverViewController*)conflictResolver {
    [self dismissViewControllerAnimated:YES completion:nil];
    [NSFileVersion removeOtherVersionsOfItemAtURL:_document.fileURL error:nil];
    NSArray* conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:_document.fileURL];
    for (NSFileVersion* fileVersion in conflictVersions) {
        fileVersion.resolved = YES;
    }
}

These methods illustrate the steps described in “How to Tell iOS That a Document Version Conflict Is Resolved.” If the chosen document is a conflict version, the delegate calls replaceItemAtURL:options:error: on the passed-in NSFileVersion object to replace the document file in the iCloud container directory with the chosen document. The delegate then enumerates the array containing NSFileVersion objects representing all conflict versions of the document and sets the resolved property of each object to YES. It then asks NSFileVersion to remove all other conflict versions of the document associated with the document’s file URL and calls revertToContentsOfURL:completionHandler: to revert the displayed document to the new contents of the document file.

The second delegation method, invoked when the current document file is selected, is much simpler. It sets the resolved property of all NSFileVersion objects representing conflict versions to YES and removes all conflict versions associated with the document file URL.