Document versions are now preserved by iCloud, unreliably

I’ve long been a fan of the built-in document version management in macOS, but frustrated at how versions weren’t preserved when sharing files in iCloud. At least, they weren’t until recently, and although this does work now, it seems riddled with bugs that will confuse you.

This all came to light because a regular reader and user of my app Revisionist noticed that it and Pages told very different stories about documents which he had saved to iCloud Drive. When he browsed all versions in the current version (7.2) of Pages, he could see many versions of his documents that didn’t appear in Revisionist’s listing of those available.

The macOS version management system centres on a hidden and locked-away folder named .DocumentRevisions-V100 at the root of each volume. Any apps on that volume that want to store previous versions do so in the database and folders within that. iCloud and iCloud Drive don’t have a .DocumentRevisions-V100 folder, but documents stored in them can retain old versions in that folder on their startup volume.

The end result is that versions are preserved only for the Mac which saves that document to iCloud. Any other Mac (or iOS device) which connects to that iCloud Drive cannot see those versions, so they are effectively lost. As Apple’s developer documentation puts it (still): “For files in the cloud, there is usually only one version of the file at any given time.”

At some stage in the last few weeks, Apple has changed this, and versions of documents in iCloud can now be preserved and accessed by other Macs and iOS devices. For this to work, the app creating the versions needs to have been built with the macOS 10.14 SDK (in Xcode 10), although it can actually be running on macOS 10.12. This requires iCloud Drive, and generally works most reliably with Mojave. If you’re using it from Pages, Numbers or Keynote, you will need to use the current release, in Pages’ case version 7.2, which requires High Sierra or later.

To test it out, open Pages 7.2, create a new document, add some content, and save it to iCloud Drive, preferably in the Pages folder there. Edit the document some more and save, again and again to build a respectable number of versions. When you use the Browse All Versions… command in the File menu, you should then see all those versions which you have saved along the way, and they should be listed in Revisionist too.

Give this a few minutes to synchronise with another Mac which is connected to the same iCloud Drive, then open that document in iCloud Drive from that other system. When you now use the Browse All Versions… command from there, you should be shown more than the current version of that document. You may well see that the most recent few versions are fully accessible, while older versions are now available to download when you click on the cloud icon, but don’t click on any just yet.

versioncloud01

Revisionist may only show one or few versions at this stage, omitting those for which you have yet to click on the cloud icon. Return to the app and click on those remaining to bring them all to local storage. Now open that document again in Revisionist, and you’ll see all those old versions in full detail.

Is there a bug in Revisionist?

No: it follows Apple’s latest developer documentation to the letter.

Document versions are the concern of the NSFileVersion class. Apple advises developers to use the methods of that class to access, create, and manage file versions. To the best of my knowledge, there is no other officially supported way of doing so, and Revisionist is well-behaved in this respect.

versioncloud04

Apple provides a Type Method, otherVersionsOfItem(at:), which returns all versions of a specified file except the current version. Again, this is one of a kind. There is another method, getNonlocalVersionsOfItem(at:completionHandler:), for which Apple provides no further information at all. That method has been available since macOS 10.10, so is hardly new either.

Using otherVersionsOfItem(at:), these versions in iCloud don’t exist, even when the app has been built using the macOS 10.14 SDK. Clearly, that isn’t what apps like Pages 7.2 are using to find document versions.

Worse, when you look at the available versions of many versioned documents in iCloud Drive, different systems will behave differently.

For example, I created a Rich Text file using a simple RTF editor of my own, DelightEd, which taps into the version support in the 10.14 SDK. I saved it six times during its creation, so should have been able to access five old versions plus the current one.

versioncloud02

On the MacBook Pro running Mojave on which the document was created, it ended up with ten old versions (each duplicated) saved in its local .DocumentRevisions-V100 folder, plus the current document. On an iMac running Sierra connected to the same iCloud Drive, my editor found and downloaded four previous versions plus the current, but one old version has mysteriously gone missing. On an iPhone running iOS 12, no old versions were found at all.

versioncloud03

Figures are completely different for a document created using Pages 7.2, but Pages 7.1 didn’t work at all and its old versions could only be seen by the Mac which had created them.

This new ability of iCloud and Apple’s iWork apps isn’t entirely undocumented. A very recent Apple support note describes how you can use it to restore previous versions of iWork documents, but doesn’t even mention that it only works with the latest releases of the iWork apps. It does, though, demonstrate how bizarre its interface is in iOS, but I’ll leave you to discover that for yourselves.

What has Apple changed?

Apple could have implemented the same hidden .DocumentRevisions-V100 folder on iCloud, but I can find no evidence that such a folder exists there, and suspect that its function is instead emulated by storing versions in a database.

When an app asks macOS for previous versions, if the document is stored on iCloud Drive, it is now given details of saved versions, allowing the user to decide which to download to local storage for preview and use. However, those remote versions are not (yet) returned by the standard otherVersionsOfItem(at:) function, so cannot be seen by Revisionist at that stage, while they remain in the cloud. As they are held in a database, Cirrus can’t see them either, so can’t spare you the tedious task of downloading them one at a time.

When a user clicks to download an old revision from the iCloud database, that is then stored in their local .DocumentRevisions-V100 folder, on their startup volume. It then becomes visible to the otherVersionsOfItem(at:) call, thus to Revisionist.

Currently, the implementation of this has at least one major bug which results in inconsistency across different clients and can cause local versions to become duplicated: in my case, one copy of each version was stored in the path /.DocumentRevisions-V100/PerUID/501/5d6/com.apple.documentVersions using a UUID as the filename, and a second with the filename of com~apple~CloudDocs_[UUID]_s[version number].

Worse, this brings chaos to local version storage. When generated and stored locally, new versions are added in that sequence. Thus, the oldest is version number 1, and the most recent is the highest number in that version set. When downloaded piecemeal from iCloud, versions are added to the local store in the order in which they are received, so version 8 may in fact be the first version, and version 1 may be that immediately prior to the current one. The only way of checking which is which is to rely on the datestamp of that version, which resolves to seconds.

The local version manager is responsible for discarding unwanted versions, for example when a document is deleted. It is unclear whether that would happen when the parent document is ‘evicted’ from local storage, or waits until it is removed altogether from iCloud Drive. That appears prone to the accumulation of orphaned versions in local storage.

The extension of document version management to iCloud Drive is a big step forward, and long overdue. However, as far as I can tell this change is undocumented in Apple’s user and developer documentation – apart from that single misleading support note – and unsupported in Apple’s public interface to macOS.

As currently implemented, it is bug-ridden and greatly confusing, and looks more like a pre-release test version than anything fit for final release to users.

(Thanks to Edoardo who sparked this off, and wouldn’t accept my half-baked excuses.)