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
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.
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
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 Coordination” 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.
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.
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.
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.
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:
Enable Auto Save. For your app to participate with iCloud, you must enable Auto Save.
In iOS, enable Auto Save by registering with the default
NSUndoManagerobject or else by calling the
updateChangeCount:at appropriate points in your code, such as when a view moves off the screen or your app transitions to the background.
In iOS, actively track document location. Each instance of your iOS app must be prepared for another instance to move, rename, or delete iCloud-based documents. If your app persistently stores a URL or path information to a file or file package, do not assume that the item will still be there the next time you attempt to access it.
In iOS, employ an
NSMetadataQueryobject, along with file coordination, to actively track the locations of your documents. Early in your app’s launch process, instantiate and configure a metadata query object, start it, and register for its
NSMetadataQueryDidUpdateNotificationnotification. Implement the
presentedItemDidMoveToURL:method and the
presentedItemURLproperty to allow your app to respond to pushed changes from iCloud. Refresh your app’s model layer and update your app’s user interface elements as needed.
For a working example of using a metadata query in iOS, see “Searching for iCloud Documents” in Your Third iOS App: iCloud. For details on using metadata queries, see File Metadata Search Programming Guide.
In OS X, do not actively track document location. The Open and Save dialogs in a document-based Mac app automatically track the locations of iCloud-based documents; you typically do not need to use an
NSMetadataQueryobject. For example, if a user renames or moves a document while working on one device, instances of your app running on other devices automatically pick up those changes by way of the document architecture.
In iOS, actively download files when required. Items in iCloud but not yet local are not automatically downloaded by iOS; only their metadata is automatically downloaded. The initial download of new iCloud-based documents requires your attention and careful design in your app. After you explicitly download such an item, the system takes care of downloading changes arriving from iCloud.
Consider keeping track of file download status as part of your iOS app’s model layer. Having this information lets you provide a better user experience: you can design your app to not surprise users with long delays when they want to open a document that is not yet local. For each file (or file package) URL provided by your app’s metadata query, get the value of the
NSURLUbiquitousItemIsDownloadedKeykey by calling the
getResourceValue:forKey:error:. Reading a file that has not been downloaded can take a long time, because the coordinated read operation blocks until the file finishes downloading (or fails to download).
For a file (or file package) that is not yet local, you can initiate download either when, or before, the user requests it. If your app’s user files are not large or great in number, one strategy to consider is to actively download all the files indicated by your metadata query. For each file (or file package) URL provided by the query, make the corresponding item local by calling the
startDownloadingUbiquitousItemAtURL:error:. If you pass this method a URL for an item that is already local, the method performs no work and returns
In iOS, respond to, and resolve, document version conflicts as needed. The document architecture supports conflict resolution by nominating a winning
NSFileVersionobject, but it is your iOS app’s responsibility to accept the suggestion or override it. You can rely on this automatic nomination most of the time, but your app should be prepared to help as needed.
A conflict arises when two instances of your iOS app, running on two different devices, attempt to change a document. This can happen, for example, if two devices are not connected to the network, a user makes changes on both of them, and then reconnects both devices to the network.
When an iOS document’s state changes, it posts a
UIDocumentStateChangedNotificationnotification. On receiving this notification, query the document’s
documentStateproperty and check if the value is
UIDocumentStateInConflict. If you determine that explicit resolution is needed, resolve the conflict by using the
NSFileVersionclass. Enlist the user’s help, if necessary; but when possible, resolve conflicts without user involvement. Keep in mind that another instance of your app, running on another device attached to the same iCloud account, might resolve the conflict before the local instance does.
When done resolving a conflict, be sure to delete any out-of-date document versions; if you don’t, you needlessly consume capacity in the user’s iCloud storage.
In OS X, rely on the system to resolve document conflicts. OS X manages conflict resolution for you when you use documents.
In OS X, avoid deadlocks resulting from modal UI elements. It is possible for a document change to arrive from iCloud while your app is presenting a modal user interface element, such as a printing dialog, for the same document. If that incoming change requires its own modal interface, such as for conflict resolution, your app can deadlock.
Always use a file coordinator to access an iCloud file or file package. A document uses a file coordinator automatically, which is one of the great benefits of using documents when designing for iCloud.
A document-based iOS app, however, must use a file coordinator explicitly when operating on a document’s underlying file; that is, when moving, renaming, duplicating, or deleting the file. For these operations, use methods from the
NSFileManagerclass within the context of a file coordinator writing method. For more information, see “The Role of File Coordination” in File System Programming Guide.
Most document-based OS X apps should employ the built-in app-centric document viewing UI that appears when opening or saving documents. Even if your OS X app needs to present documents programmatically to the user, do not programmatically move, rename, or delete documents. It’s up to the user to perform those operations using the Finder, the built-in document renaming UI, or the built-in iCloud “move” menu items in the File menu.
Don’t let users unintentionally share information. Unlike iOS users, OS X users have direct access to the file system. For example, if you keep an undo stack within your document format’s file package, that information is accessible to a user viewing your app’s document in OS X.
This issue predates iCloud, but may not be familiar to you if you are primarily an iOS developer.
When designing your document format, carefully consider which information your users would not want to share—for example, in an emailed version of the document. Instead of placing such information within the file package, associate it with the document but store it outside of the
Documentssubdirectory in the ubiquity container (see Figure 1-2).
Make your documents user-manageable, when appropriate. Place files in the
Documentssubdirectory of an iCloud container to make them visible to the user and make it possible for the user to delete them individually. Files you place outside of the
Documentssubdirectory are grouped together as “data.” When a user visits System Preferences (OS X) or Settings (iOS), they can delete content from iCloud. Files that you place outside of the
Documentssubdirectory can be deleted by the user only as a monolithic group.
The choice of whether or not to use the
Documentssubdirectory depends on your app design. For example, if your app supports a user creating and naming a document, put the corresponding document file in the
Documentssubdirectory. If your app does not let users interact with files as discrete documents, it’s more appropriate to place them outside of the
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
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 (
NSColor), images (
NSImage), and Bezier paths (
NSBezierPath), as well as for other classes.
For example, an OS X
NSColorobject 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.
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
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 (
NSColor) has an initialization method that lets you create a color object from a Core Image
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
NSCoderor 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.txtand the file
MyDoc.TXTcannot 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.
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.
Create a new standard document
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
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
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
Automatic (OS X)
In iOS, documents detect and notify you about conflicts. Use
Move, duplicate, and delete iCloud-based documents
Manipulate files on disk using the
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.