Designing for Documents in iCloud

Adopting iCloud document storage makes your app’s documents available on all of a user’s devices.

To your app, a document (based on the UIDocument class in iOS or the NSDocument class in OS X) is an assemblage of related data that can be written to disk as a single file or as a file package. A file package, in turn, is a directory presented to the user as a single file, accessible to your app using an NSFileWrapper object.

Documents automatically implement most of the behavior expected of iCloud apps. Specifically, a document automatically ensures that local changes are safely coordinated with iCloud-originated changes. It does this by employing a file coordinator (NSFileCoordinator) object and by adopting the file presenter (NSFilePresenter) protocol. In OS X, starting in version 10.8 Mountain Lion, documents automatically provide open/save/rename UI and functionality; in iOS your app must implement these things.

How iCloud Document Storage Works

Your app uses iCloud document storage by reading and writing to a Documents subdirectory of one of its ubiquity containers. Figure 3-1 shows a simplified representation of local storage on a device, and shows data transfer to and from the iCloud servers.

Figure 3-1  Data and messaging interactions for iCloud apps

The system automatically grants your app access to the data in your app’s own sandbox container, but your app must request entitlements and employ iCloud storage APIs to use a ubiquity container. Figure 3-1 assumes that you have previously requested the appropriate entitlements, as explained in “Request Access to iCloud Storage By Using Entitlements.”

At step 1 in Figure 3-1, your app calls the NSFileManager method URLForUbiquityContainerIdentifier:, on a background thread, to set up iCloud storage and to obtain the URL for the specified ubiquity container. You must perform this step explicitly, except in the case of a document-based app in OS X, for which your document objects do this automatically on your behalf.

At step 2, the system grants your app access to the ubiquity container. Finally, at step 3, your app can read and write data to iCloud by accessing the ubiquity container.

Behind the scenes, the system manages the transfer of your app’s iCloud data to and from the iCloud servers in a way that preserves data integrity and consistency. In particular, the system serializes those transfers with your app’s file-system access, preventing conflicts from simultaneous changes. To support this serialization, iCloud apps must use special objects called file coordinators and file presenters. The name for serialized file system access using these objects is file coordination.

A document automatically uses a file coordinator (an instance of the NSFileCoordinator class) and adopts the file presenter (NSFilePresenter) protocol to manage its underlying file or file package. If you directly access a file or file package, such as to create it or delete it, you must use file coordination explicitly. For more information about using file coordination, see “The Role of File Coordinators and Presenters” in File System Programming Guide.

The first time your app adds a document’s on-disk representation to a ubiquity container, the system transfers the entire file or file package to the iCloud server, as shown in Figure 3-2.

Figure 3-2  Transferring a file to iCloud the first time

This transfer proceeds in two steps. In step 1 in the figure, the system sends the document’s metadata, which includes information such as the document name, modification date, file size, and file type. This metadata transfer takes place quickly. At some later point, represented in step 2, the system sends the document’s data.

Sending document metadata first lets iCloud quickly know that a new document exists. In response, the iCloud server propagates the metadata quickly to all other available devices attached to the same iCloud account. This lets the devices know that a new document is available.

After a document’s data is on the iCloud server, iCloud optimizes future transfers to and from devices. Instead of sending the entire file or file package each time it changes, iCloud sends only the metadata and the pieces that changed. Figure 3-3 shows a visual representation of an incremental upload.

Figure 3-3  Transferring just the file changes to iCloud

Again, file metadata is sent quickly to the iCloud servers, as shown in step 1, so that it can be propagated to other devices. Because the system tracks changes to the document, it is able to upload only the parts that changed, as shown in step 2. This optimization reduces iCloud network traffic and also reduces the amount of power consumed by the device, which is important for battery-based devices. As you learn later in this chapter, in “Design for Network Transfer Efficiency,” good file format design is essential to supporting incremental transfer.

Downloading of a document from iCloud to a device works as follows: Say a user creates a document on one device, as you saw in Figure 3-2. iCloud quickly sends that document’s metadata to all other devices attached to the same account. Step 1 of Figure 3-4 represents this transfer of metadata to one of those other devices.

Figure 3-4  Receiving a file from iCloud for the first time

The details of step 2 in this figure depend on the receiving device’s type.

An iOS device does not automatically download the new file, so your app must help. The steps to perform are described in “App Responsibilities for Using iCloud Documents.”

On a Mac, download is automatic for files created by other devices on the same account. For this reason, a Mac is sometimes referred to as a “greedy peer.” If your Mac app sees metadata for a new document but the document itself is not yet local, it is likely already being downloaded by the system on your app’s behalf.

Figure 3-5 depicts a device receiving updated metadata for a changed document that the device has previously downloaded. New metadata is received, in this scenario, because the user made a change to the document on another of their devices.

Figure 3-5  Receiving changed data from iCloud

When the device receives updated metadata (step 1), the system responds by asking iCloud to send the corresponding incremental data right away (step 2).

App Responsibilities for Using iCloud Documents

Changes to your app’s documents can arrive from iCloud at any time, so your app must be prepared to handle them. The NSDocument class in OS X does most of this work for you, while in iOS there is more for your app to handle explicitly. Follow these guidelines:

Designing a Document File Format for iCloud

Choices you make in designing your document format can impact network transfer performance for your app’s documents. The most important choice is to be sure to use a file package for your document format.

If you provide versions of your app for iOS and Mac, design your document file format to be cross-platform compatible.

Design for Network Transfer Efficiency

If your document data format consists of multiple distinct pieces, use a file package for your document file format. A file package, which you access by way of an NSFileWrapper object, lets you store the elements of a document as individual files and folders that can be read and written separately—while still appearing to the user as a single file. The iCloud upload and download machinery makes use of this factoring of content within a file package; only changed elements are uploaded or downloaded.

Register your document file format, and its associated filename extension, in your app’s Info.plist property list file in Xcode. Specifically, use the “CFBundleDocumentTypes” in Information Property List Key Reference key to specify the file formats that your app recognizes and is able to open. Specify both a filename extension and a uniform type identifier (UTI) corresponding to the file contents. The system uses this information to associate file packages to your app, and, in OS X, to display file packages to the user as though they were normal files.

Design for Persistent Document State

Many document-based apps benefit from maintaining state on a per-document basis. For example, a user of a vector-based drawing app would want each document to remember which drawing tool was most recently used and which element of the drawing was selected.

There are two places you can store document-specific state:

  • Within the file package (or flat-file format) for a document. This choice supports keeping the state together with the document if, for example, a user sends it by email.

  • Associated with the document but outside of its file package (or file format). This choice supports cases in which a user would not want to share information. But, being outside of the data managed by your document objects, such state is not tracked by a document’s conflict resolution functionality. It is up to you to do so.

Whichever scheme you choose, take care, in an app that supports editing, to never save document state unless document content was edited. Otherwise, you invite trivial and unhelpful conflict scenarios that consume network bandwidth and battery power.

For example, imagine that a user had been editing a long text document on their iPad, working on page 1. A bit later, they open the document on their iPhone and scroll to the last page. This badly-behaved example app aggressively saves the end-of-document scroll position—even though the user made no other changes. When the user later opens the document on their iPad to resume editing, there is a needless conflict due to scroll position data. The system has marked the document as in conflict (UIDocumentStateInConflict), and the newer version (automatically nominated as the conflict winner) is the one scrolled to the last page. To be a well-behaved iCloud app, this text editor should have ignored the change in scroll position on the iPhone because the user did not edit the content.

Think through various usage scenarios for your app, and design accordingly to improve user experience. Take care with state like:

  • Document scroll position

  • Element selection

  • Last-opened timestamps

  • Table sort order

  • Window size (in OS X)

A strategy to consider is to save such state but only when the user has also made a change that deserves saving. In OS X, the built-in Resume feature provides all the state saving behavior that most document-based apps need. If you want additional control, you can take advantage of the NSChangeDiscardable document change type.

Design for Robustness and Cross-Platform Compatibility

Keep the following factors in mind when designing a document file format for iCloud:

  • Employ a cross-platform data representation. Like-named classes—those whose prefixes are “UI” for iOS and “NS” for OS X—are not directly compatible. This is true for colors (UIColor and NSColor), images (UIImage and NSImage), and Bezier paths (UIBezierPath and NSBezierPath), as well as for other classes.

    For example, an OS X NSColor object is defined in terms of a color space (NSColorSpace), but there is no color space class in iOS.

    If you employ such a class in a document format property, you must devise an intermediate iCloud representation that you can faithfully reconstitute into the native representation on either platform, as depicted in Figure 3-6.

    Figure 3-6  Using a cross-platform data representation

    In step 1, your OS X app stores an in-memory document object to disk, converting each platform-specific data type to an appropriate cross-platform representation. Steps 2 and 3 show the data uploaded and then downloaded to an iOS device. In step 4, the iOS version of your app extracts the data, converting from the intermediate representation into iOS-specific data types, thereby constructing a UIDocument-based version of the original NSDocument-based object.

    When storing a document from your iOS app for later viewing on either platform, you’d reverse the process shown in Figure 3-6—starting by storing your iOS document using a cross-platform data representation.

    For each platform-specific data type you employ in your document format, check if there is an appropriate, lower-level data type shared by both platforms that you can use in the intermediate representation. For example, each color class (UIColor and NSColor) has an initialization method that lets you create a color object from a Core Image CIColor instance.

    When preparing data for writing to iCloud, convert to the intermediate representation; when reading an iCloud-based file, convert from the intermediate representation. If you call methods from NSCoder or its concrete subclasses to encode and decode your document’s object graph, perform these conversions within those methods.

  • Take platform-specific coordinate systems into account. The default screen coordinate systems in iOS and OS X are different, leading to differences in how you draw and in how you position views. Take this into account when storing and extracting screen coordinate information with your cross-platform representation. For more information, see “Coordinate Systems and Drawing in iOS” in Drawing and Printing Guide for iOS.

  • Always use case-insensitive filenames. OS X, by default, and for nearly all users, employs a case-insensitive file system. For example, the file mydoc.txt and the file MyDoc.TXT cannot both exist within the same directory in OS X. By contrast, iOS treats those file names as being different.

    To make your document file format cross-platform compatible, you must read and write files in a case-insensitive manner.

  • Use a format version number. Because you may want to change your document format in the future, design a format version numbering scheme and apply the version number as a property of the document format.

    Versioning your document format has always been a good idea, but with iCloud and cross-platform formats, it’s even more important. For example, an iCloud user is likely to have multiple devices on which they can view their content; but the user might not conscientiously update your app on all of their devices at once. So, for example, a user’s Mac might have the newest version of your OS X app but their iPad might have a year-old version of your iOS app.

    An early version of your app might be iOS-only; when you later distribute an OS X version, you’ll likely want to modify the document format. By embedding the format version within each document, your code can gracefully handle documents regardless of their version. You might, for example, make an old, iOS-only format read-only in OS X. Think through various scenarios involving multiple devices on each platform, each running a different version of your app. Make sure that you provide the best possible user experience for iCloud-based documents.

For more information about techniques for designing file formats, see “Choosing Types, Formats, and Strategies for Document Data” in Document-Based App Programming Guide for iOS or “Handling a Shared Data Model in OS X and iOS” in Document-Based App Programming Guide for Mac.

Document-Based Workflows

Table 3-1 lists some typical workflows for document-based apps. For each workflow, the table lists the primary classes you typically use, along with a brief description of the task.

Table 3-1  Typical document-based app workflows

Workflow

Implementation

Description

Create a new standard document

UIDocument (iOS)

NSDocument (OS X)

Use a document object to create and manage the data structures in your document format. The document classes automatically support saving new documents to a ubiquity container or to local storage.

Create a new Core Data document

UIManagedDocument (iOS)

NSPersistentDocument (OS X)

Use the Core Data document subclasses to create and manage your Core Data stores. For details, see “Designing for Core Data in iCloud.”

Obtain URLs to iCloud documents

NSMetadataQuery (iOS)

Automatic (OS X)

In iOS, use a metadata query object to locate and obtain live-updated information on iCloud documents.

In OS X v10.8 or later, a document Open dialog uses a metadata query automatically.

Prompt the user to open a document.

Custom UI (iOS)

Automatic as part of the document architecture (OS X)

In iOS, your app is directly responsible for presenting a selection UI for user documents in a simple and clean way that fits well with the rest of the app design.

In OS X v10.8 or later, the Open command in a document-based app presents a dialog that lets the user select iCloud files.

Handle version conflicts

UIDocument, NSFileVersion (iOS)

Automatic (OS X)

In iOS, documents detect and notify you about conflicts. Use NSFileVersion objects (one per revision of a document) to resolve them, as needed.

Move, duplicate, and delete iCloud-based documents

NSFileCoordinator

NSFileManager

Manipulate files on disk using the NSFileManager class, and always within the context of a file coordinator object.

For details on how to perform the preceding workflows, look in the document-based programming guide for the platform you are targeting. For iOS, see Document-Based App Programming Guide for iOS. For Mac, see Document-Based App Programming Guide for Mac.