Designing for CloudKit
CloudKit is a way to move structured data between your app and iCloud. Whereas document-based storage operates on files, CloudKit operates on records. A record is a dictionary of key-value pairs, with each key representing a field of the record. The value of each field is usually a simple data type such as a string, number, or date, but you can also store entire files or arbitrary blocks of data. Fields can also store references to other records, making it possible to define relationships between records.
With CloudKit, you decide when to move data from your app to iCloud and from iCloud to your app. Although CloudKit provides facilities to keep you informed when changes occur, you must still fetch those changes explicitly. Because you decide when to fetch and save data, you are responsible for ensuring that data is fetched at the right times and in the right order, and you are responsible for handling any errors that arise.
For detailed information about the classes of the CloudKit framework, see CloudKit Framework Reference. For how to enable CloudKit in your Xcode project and access CloudKit Dashboard, read CloudKit Quick Start.
Before you can use CloudKit, your app must enable the iCloud capability for your app, as described in Request Access to iCloud Using Xcode Capabilities. Configuring the CloudKit service adds the appropriate entitlements to your app and configures your app with a default container based on its bundle ID.
Containers and Databases
Like other iCloud technologies, CloudKit organizes data using containers. A container serves as the local representation of your app’s iCloud storage. When starting your project, you request entitlements for each of their containers. At runtime, you can perform tasks against a specific container using a
Each container is divided into public and private databases, each of which is represented by a
CKDatabase object. Any data written to the private database is visible only to the current user and is stored in that user’s iCloud account. Data written to the public database is visible to all users of the app and is stored in the app’s iCloud storage.
For a running CloudKit app, a container’s public database is always readable, even when there is no active iCloud account configured on the device. Saving records to the public database and accessing the private database in any way requires the presence of an active iCloud account on the device. If your app does more than read data from the public database, check for an active iCloud account at startup and adjust your interface accordingly. For example, if no iCloud account is available, disable any UI for saving records and notify the user that saving records requires an active iCloud account.
For information about how to check for the availability of an iCloud account and respond to changes to the current account, see Prepare Your App to Use iCloud.
Managing Data in CloudKit
Inside the public and private databases, you organize your app’s data using records and use them to move data between your app and iCloud. Records are dictionaries of key-value pairs represented by the
CKRecord class. Each record has a record type, which is a string you use to differentiate between records with different types of information. Each key-value pair represents a field of the record and that field’s corresponding value. Fields can contain simple types such as strings, numbers, and dates or more complex types such as locations, references to other records, or entire files.
Apps must fetch the records they need to run and save changes to those records explicitly. Nothing is automatic. You can initiate fetch and save operations using either operation objects or the convenience methods of the
CKDatabase classes. Operation objects can operate on multiple records at once and can be configured with dependencies to ensure that records are saved in the proper order. Operation objects are based on the
NSOperation class and can be integrated with your app’s other workflows easily. Fetch operations require that you already know the ID of the record you want.
If you do not know the ID of a record, CloudKit provides the ability to search for records using a predicate. A predicate-based search lets you locate records whose fields contain certain values. You use this predicate with a
CKQuery object to specify both the search criteria and sorting options for the returned records. You then execute the query using a
Another search option is to let the server notify you when certain parts of the database change. Subscriptions act like a persistent query on the server. You configure a
CKSubscription object much like you do a
CKQuery object, but instead of executing that query explicitly, you save the subscription to the server only once. After that, the server sends push notifications to your app whenever a change occurs that matches your search criteria. For example, you can use subscriptions to detect the creation or deletion of records or to detect when the field of a record changed to a specific value. Upon receiving the push notification from the server, you can fetch the changed record and update your app.
For detailed information about how to use records, operation objects, and the other classes of the CloudKit framework, see CloudKit Framework Reference.
The Development and Production Environments
CloudKit provides separate development and production environments for your record types and data. The development environment is a more flexible environment that is available only to members of your development team. When your app adds a new field to a record and saves that record in the development environment, the server updates the schema information automatically. You can use this feature to make changes to your schema during development, which saves time. One caveat is that after you add a field to a record, the data type associated with that field cannot be changed programmatically. To change a field’s type, you must delete the field in CloudKit Dashboard and add it again with the new type.
Prior to deploying your app, you migrate your schema and data to the production environment using CloudKit Dashboard. When running against the production environment, the server prevents your app from changing the schema programmatically. You can still make changes with CloudKit Dashboard but attempts to add fields to a record in the production environment result in errors.
During development, Xcode automatically points your app to the development environment. Before you ship your app, configure your app using the distribution workflow. In this workflow, Xcode lets you choose whether you want to target the development or production environment and adds the
com.apple.developer.icloud-container-environment entitlement to your app with the value you selected. Prior to shipping, be sure to configure your app for the production environment. Apps that target the development environment will be rejected by the App Store.
The Basic CloudKit Workflow
In your app, you use CloudKit to retrieve data from iCloud, make changes, and write those changes back. The process for retrieving data from a CloudKit database is as follows:
CKContainerobject corresponding to the iCloud container whose data you want to access.
CKDatabaseobject that corresponds to the database (public or private) that contains the records.
Identify the records you want to fetch:
Assign blocks to the appropriate completion handlers of your operation object. Use your blocks to process the results.
Pass your operation object to the
addOperation:method of your
CKDatabaseobject. Calling this method enqueues the operation for execution. The operation object then delivers the results asynchronously to your completion blocks.
To save a record that you have created or modified, create a
CKModifyRecordsOperation object with the record that you want to save. Execute that operation object in the same way you did when fetching records, by calling the
addOperation: method of the appropriate
CKDatabase object. As with fetch operations, a save operation executes asynchronously and reports status to the completion handler blocks you specify. Use those blocks to process the results and manage any errors.
It is your responsibility to decide when to fetch the records you need and when to save your changes; your role also includes ensuring the validity of the data you save. CloudKit saves each record atomically. If you need to save a group of records in a single atomic transaction, save them to a custom zone, which you can create using the
CKRecordZone class. Zones are a useful way to arrange a discrete group of records but are supported only in private databases. Zones cannot created in a public database.
Tips for Defining Your App’s Record Types
When defining your app’s record types, it helps to understand how you use those record types in your app. Here are some tips to help you make better choices during the design process:
Organize your records around a central record type. A good organization strategy is to define one primary record type and additional record types to support the primary type. Using this type of organization lets you focus your queries on your primary record type and then fetch any supporting objects as needed. If you define too many primary record types, your app’s interface may require more complexity to select or search for relevant records. For example, a calendar app might define a single calendar record (the primary record type) that contains multiple event records (a secondary record type) corresponding to events on that calendar.
Use references to create strong relationships between records. Although you can store the ID of a record or asset in a field using an
NSStringobject, doing so is not recommended. Instead, use a
CKReferenceobject to establish a formal relationship between the two objects. References also let you establish an ownership model between objects that can make deleting those records easier.
Include version information in your records. A version number can help you decide at runtime what information might be available in a given record.
Handle missing keys gracefully in your code. For each record you create, you are not required to provide values for all keys contained in the record type. For any keys you do not specify, the corresponding value is
nil. Your app should be able to handle keys with
nilvalues in an appropriate way. Being able to handle missing keys becomes important when new versions of your app access records created by an older version.
Avoid complex graphs of records. Creating a complex set of relationships between records creates the potential for problems later when you need to update or delete records. If the owner relationships among your records are complex, it might be difficult to delete records later without leaving other records in an inconsistent state.
Use assets for discrete data files. When you want to associate images or other discrete files with a record, use a
CKAssetobject to do so. The total size of a record’s data is limited to 1 MB though assets do not count against that limit.
Future Proofing Your Records
As you design the record types for your app, make sure those records meet your needs but do not restrict you from making changes in the future. It is easy to add new fields to a record, however, a field cannot be removed or its data types changed after you deploy your records to a production environment. There are some ways that you can make updating your app easier in the future:
Use new keys to represent new data types. Adding new keys to records is a simple process. A new version of your app can incrementally update records as it touches them by adding whatever keys it needs.
Define record types that do not lose data integrity easily. Each new version of your app must create records that do not break older versions of the app. The best way to ensure data integrity is to minimize the amount of validation required for a record:
Avoid fields that have a narrow range of acceptable values, the changing of which might cause older versions of the app to treat the data as invalid.
Avoid fields whose values are dependent on the values of other fields. Creating dependent fields means you have to write validation logic to ensure the values of those fields are correct. Once created, this kind of validation logic is difficult to change later without breaking older versions of your app.
Minimize the number of required fields for a given record. As soon as you require the presence of a field, every version of your app after that must populate that field with data. Treating fields as optional gives you more flexibility to modify your data later.
Handle missing keys gracefully. If a key is not present, add it quietly.