Designing for Documents in iCloud

Adopting iCloud document storage makes your app’s documents available on all of a user’s devices. 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 and made accessible to your app through 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 v10.8 and later, documents automatically provide open/save/rename UI and functionality; in iOS your app must implement these things. The use of file coordinators and presenters is mandatory when working with iCloud documents.

How iCloud Document Storage Works

When you write files and directories to iCloud document storage, the system transfers those items automatically to iCloud and to the user’s other devices. Using iCloud document storage is similar to using the local file system, except for the following requirements:

To understand how iCloud document storage works, it helps to see it in action. Figure 3-1 shows a simplified representation of local storage on a device. In addition to the app’s local data directories, apps can also access any iCloud containers for which they have the proper entitlements. The iCloud containers themselves also live on the device but are in a different part of the file system and must be configured before they can be used. To configure an iCloud container for use, you call the URLForUbiquityContainerIdentifier: method of NSFileManager from a background thread (step 1), specifying the container that you want to access. The system configures the container (step 2) and returns the base URL of the container directory. You use that URL to build further URLs to specify files and directories, and you use the URL to create metadata queries to search the container (step 3).

Figure 3-1  Data and messaging interactions for iCloud apps

Using File Coordination to Access Files and Directories

When accessing files and directories in a container, iCloud apps are required to use file coordination to do so. File coordination uses file coordinator and file presenter objects to serialize access to files and directories in order to preserve data integrity. File presenters monitor a file and are notified whenever another thread or process takes action on it. All actions on a file must happen through a file coordinator, which coordinates those actions with any interested file presenters. For example, when moving a file from one location to another, the file coordinator notifies any interested file presenters of the new location. And when writing to a file, the file coordinator delays the write operation until all file presenters indicate that it is safe to do so.

Apps that use document objects (UIDocument in iOS and NSDocument in OS X) get file coordination for free. Document objects implement the presenter (NSFilePresenter) protocol and use its methods to manage the underlying file or file package. When reading or writing the file contents, document objects also use file coordinators (instances of the NSFileCoordinator class) automatically. If you are not using document objects to access files, you must handle the file coordination yourself. For more information about how to support file coordination in your own objects, see The Role of File Coordinators and Presenters in File System Programming Guide.

Transferring Data to and from iCloud

The first time your app adds a document’s on-disk representation to an iCloud (ubiquity) container, the system transfers the entire file or file package to the iCloud server, as shown in Figure 3-2. The first step is to send 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. The second step is to send the document’s data. Sending the document metadata first lets iCloud know quickly that a new document exists. The iCloud server propagates new and changed metadata quickly to all other available devices attached to the same iCloud account. This lets the devices know that a new document is available.

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

After a document’s data is on the 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 this incremental upload. As before, the system sends the file metadata first so that iCloud can propagate it to other devices. After sending the metadata, the system uploads only the portions of the document that changed. This optimization reduces iCloud network traffic and reduces the amount of power consumed by the device. You can help this optimization by designing file formats that support incremental transfers, as described in Design for Network Transfer Efficiency.

Figure 3-3  Transferring just the file changes to iCloud

When a new file appears in iCloud, that file must be downloaded to the user’s other devices, as shown in Figure 3-4. For each device, iCloud first sends that document’s metadata so that the device knows of the file’s existence. After that, the timing for retrieving the file data depends on the type of device. In iOS, apps must ask the system (either explicitly or implicitly) to download the file. You implicitly download a file by trying to access that file by using methods in the NSFileCoordinator class, or you explicitly download the file by invoking the startDownloadingUbiquitousItemAtURL:error: method in the NSFileManager class. A Mac downloads files automatically as soon as it detects them on the server. 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-4  Receiving a file from iCloud for the first time

After downloading a file’s contents the first time, subsequent operations download only the portions of the file that changed, as shown in Figure 3-5. As with all transfers, the first step in the transfer process is to download the updated document metadata for the file. After receiving the metadata, devices automatically pull changes at appropriate times. On iOS devices, changes are pulled at appropriate times, such as when the app that owns the files comes to the foreground. In OS X, changes are pulled immediately.

Figure 3-5  Receiving changed data from iCloud

To check the upload or download progress of a file, retrieve the value of the NSURLUbiquitousItemDownloadingStatusKey from the corresponding NSURL object. The value of that key tells you whether the file is already local to the device or in the process of being downloaded. You can receive more specific data about the download progress by getting other attributes of the NSURL object. For a list of relevant keys, see NSURL Class Reference.

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 but in iOS there is more for you to handle explicitly. When implementing your iOS and OS X apps, do the following to make sure your apps handle iCloud changes appropriately:

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 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 are 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 her iPad, working on page 1. A bit later, she opens the document on her iPhone and scrolls 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 her 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. When the NSDocument object saves its data to disk (step 1), it converts each platform-specific data type to an intermediate cross-platform representation. That cross-platform data is saved to iCloud and downloaded to the user’s other devices (steps 2 and 3). When the iOS version of your app extracts that data (step 4), it converts the data from the cross-platform representation into an iOS-specific version.

    Figure 3-6  Using a cross-platform data representation

    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 filenames 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, and with iCloud and cross-platform formats, it’s even more important. For example, an iCloud user is likely to have multiple devices on which to view content; but the user might not conscientiously update your app on all of her devices at once. So, for example, a user’s Mac might have the newest version of your OS X app but his 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 an iCloud 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.

Enabling Document Storage in iCloud Drive

To store your documents in iCloud Drive, enable iCloud in the Capabilities pane and add the NSUbiquitousContainers key to the Info.plist file.

To specify your app’s containers, set the NSUbiquitousContainers key to your app’s container IDs. Also, specify how you want to share each container using other NSUbiquitous… keys.

<key>NSUbiquitousContainers</key>
    <dict>
        <key>iCloud.com.example.MyApp</key>
        <dict>
            <key>NSUbiquitousContainerIsDocumentScopePublic</key>
            <true/>
            <key>NSUbiquitousContainerSupportedFolderLevels</key>
            <string>Any</string>
            <key>NSUbiquitousContainerName</key>
            <string>MyApp</string>
        </dict>
    </dict>

These settings allow iCloud Drive to provide public access to the files stored in your app’s container. iCloud Drive will create a folder for your app in the user’s iCloud Drive folder to store these documents. For a detailed description of NSUbiquitous… keys, see Cocoa Keys in Information Property List Key Reference.