The CloudKit Framework is one of the more remarkable developer features available in the iOS SDK solely because of the ease with which it allows for the structured storage and retrieval of data on Apple’s iCloud database servers.
It is not an exaggeration to state that CloudKit allows developers to work with cloud-based data and media storage without any prior database experience and with minimal coding effort.
This chapter will provide a high-level introduction to the various elements that make up CloudKit, build a foundation for the CloudKit tutorials presented in the following two chapters and provide a basis from which to explore other capabilities of CloudKit.
An Overview of CloudKit
The CloudKit Framework provides apps with access to the iCloud servers hosted by Apple. It provides an easy-to-use way to store, manage and retrieve data and other asset types (such as large binary files, videos, and images) in a structured way. This provides a platform for users to store private data and access it from multiple devices and for the developer to provide publicly available data to all app users.
The first step in learning to use CloudKit is understanding the key components that constitute the CloudKit framework.
CloudKit Containers
Each CloudKit-enabled app has at least one container on iCloud. The container for an app is represented in the CloudKit Framework by the CKContainer class, and it is within these containers that the databases reside. Containers may also be shared between multiple apps.
A reference to an app’s default cloud container can be obtained via the default property of the CKContainer class:
let container = CKContainer.default
CloudKit Public Database
Each cloud container contains a single public database. This is the database into which is stored data that all users of an app need. A map app, for example, might have a set of data about locations and routes that apply to all app users. This data would be stored within the public database of the app’s cloud container.
CloudKit databases are represented within the CloudKit Framework by the CKDatabase class. A reference to the public cloud database for a container can be obtained via the publicCloudDatabase property of a container instance:
let publicDatabase = container.publicCloudDatabase
CloudKit Private Databases
Private cloud databases are used to store data that is private to each specific user. Each cloud container, therefore, will contain one private database for each app user. A reference to the private cloud database can be obtained via the privateCloudDatabase property of the container object:
let privateDatabase = container.privateCloudDatabase
Data Storage and Transfer Quotas
Data and assets stored in an app’s public cloud database count against the app’s storage quota. On the other hand, anything stored in a private database is counted against the iCloud quota of the corresponding user. Applications should, therefore, try to minimize the amount of data stored in private databases to avoid users having to unnecessarily purchase additional iCloud storage space.
At the time of writing, each application is provided with 1PB of free iCloud storage for public data for all of its users.
Apple also imposes limits on the volume of data transfers and the number of queries per second that are included in the free tier. While official documentation on these quotas and corresponding pricing is hard to find, it is unlikely that the average project will encounter these restrictions.
CloudKit Records
Data is stored in public and private databases in the form of records. Records are represented by the CKRecord class and are essentially dictionaries of key-value pairs where keys are used to reference the data values stored in the record. A wide range of data types can be stored in a record, including strings, numbers, dates, arrays, locations, data objects, and references to other records. In addition, new key-value fields may be added to a record without performing any database restructuring.
Records in a database are categorized by a record type which must be declared when the record is created and takes the form of a string value. In practice, this should be set to a meaningful value that assists in identifying the purpose of the record type. Records in a cloud database can be added, updated, queried, and deleted using a range of methods provided by the CKDatabase class.
The following code demonstrates the creation of a CKRecord instance initialized with a record type of “Schools” together with three key-value pair fields:
let myRecord = CKRecord(recordType: "Schools") myRecord.setObject("Silver Oak Elementary" as CKRecordValue?, forKey: "schoolname") myRecord.setObject("100 Oak Street" as CKRecordValue?, forKey: "address") myRecord.setObject(150 as CKRecordValue?, forKey: "studentcount")
Once created and initialized, the above record could be saved via a call to the save method of a database instance as follows:
publicDatabase.save(myRecord, completionHandler: ({returnRecord, error in if let err = error { // save operation failed } else { // save operation succeeded } }))
The method call passes through the record to be saved and specifies a completion handler as a closure expression to be called when the operation returns.
Alternatively, a group of record operations may be performed in a single transaction using the CKModifyRecordsOperation class. This class also allows timeout durations to be specified for the transaction and completion handlers to be called at various stages during the process. The following code, for example, uses the CKModifyRecordsOperation class to add three new records and delete two existing records in a single operation. The code also establishes timeout parameters and implements all three completion handlers. Once the modify operation object has been created and configured, it is added to the database for execution:
let modifyRecordsOperation = CKModifyRecordsOperation( recordsToSave: [myRecord1, myRecord2, myRecord3], recordIDsToDelete: [myRecord4, myRecord5]) let configuration = CKOperation.Configuration() configuration.timeoutIntervalForRequest = 10 configuration.timeoutIntervalForResource = 10 modifyRecordsOperation.configuration = configuration modifyRecordsOperation.perRecordCompletionBlock = { record, error in // Called after each individual record operation completes } modifyRecordsOperation.perRecordProgressBlock = { record, progress in // Called to update the status of an individual operation // progress is a Double value indicating progress so far } modifyRecordsOperation.modifyRecordsCompletionBlock = { records, recordIDs, error in // Called after all of the record operations are complete } privateDatabase?.add(modifyRecordsOperation)
It is important to understand that CloudKit operations are predominantly asynchronous, enabling the calling app to continue functioning. At the same time, the CloudKit Framework works in the background to handle the transfer of data to and from the iCloud servers. In most cases, therefore, a call to CloudKit API methods will require that a completion handler be provided. This handler code will then be executed when the corresponding operation completes and passed results data where appropriate or an error object in the event of a failure. Given the asynchronous nature of CloudKit operations, it is essential to implement robust error handling within the completion handler.
The steps involved in creating, updating, querying, and deleting records will be covered in greater detail in the next chapter entitled An iOS 17 CloudKit Example.
The overall concept of an app cloud container, private and public databases, and records can be visualized as illustrated in Figure 50-1:
CloudKit Record IDs
Each CloudKit record has associated with it a unique record ID represented by the CKRecordID class. If a record ID is not specified when a record is first created, one is provided for it automatically by the CloudKit framework.
CloudKit References
CloudKit references are implemented using the CKReference class and provide a way to establish relationships between different records in a database. A reference is established by creating a CKReference instance for an originating record and assigning the record to which the relationship is to be targeted. The CKReference object is stored as a key-value pair field in the originating record. A single record can contain multiple references to other records.
Once a record is configured with a reference pointing to a target record, that record is said to be owned by the target record. When the owner record is deleted, all records that refer to it are also deleted, and so on down the chain of references (a concept referred to as cascading deletes).
CloudKit Assets
In addition to data, CloudKit may also be used to store larger assets such as audio or video files, large documents, binary data files, or images. These assets are stored within CKAsset instances. Assets can only be stored as part of a record, and it is not possible to directly store an asset in a cloud database. Once created, an asset is added to a record as another key-value field pair. The following code, for example, demonstrates the addition of an image asset to a record:
let imageAsset = CKAsset(fileURL: imageURL) let myRecord = CKRecord(recordType: "Vacations") myRecord.setObject("London" as CKRecordValue?, forKey: "city") myRecord.setObject(imageAsset as CKRecordValue?, forKey: "photo")
Record Zones
CloudKit record zones (CKRecordZone) provide a mechanism for relating groups of records within a private database. Unless a record zone is specified when a record is saved to the cloud, it is placed in the default zone of the target database. Custom zones can be added to private databases and used to organize related records and perform tasks such as writing to multiple records simultaneously in a single transaction. Each record zone has a unique record zone ID (CKRecordZoneID) that must be referenced when adding new records to a zone.
Adding a record zone to a private database involves the creation of a CKRecordZone instance initialized with the name to be assigned to the zone:
let myRecordZone = CKRecordZone(zoneName: "MyRecordZone")
The zone is then saved to the database via a call to the save method of a CKDatabase instance, passing through the CKRecordZone instance together with a completion handler to be called upon completion of the operation:
privateDatabase.save(myRecordZone, completionHandler: ({returnRecord, error in if let err = error { // Zone creation failed } else { // Zone creation succeeded } }))
Once the record zone has been established on the cloud database, records may be added to that zone by including the record type and a record ID when creating CKRecord instances:
let myRecord = CKRecord(recordType: "Addresses", recordID: CKRecord.ID(zoneID: zoneId))
In the above code, the zone ID is passed through to the CKRecord.ID initializer to obtain an ID containing a unique CloudKit-generated record name. The following is a typical example of a CloudKit-generated record name:
88F54808-2606-4176-A004-7E8AEC210B04
To manually specify the record name used within the record ID, modify the code to read as follows:
let myRecord = CKRecord(recordType: "Houses", recordID: CKRecord.ID(recordName: "MyRecordName", zoneID: zoneId))
However, when manually specifying a record name, care should be taken to ensure each has a unique name. When the record is saved to the database, it will be associated with the designated record zone.
CloudKit Sharing
A CloudKit record contained within the public database of an app is accessible to all users of that app. Situations might arise, however, where a user wants to share specific records within a private database with others. This was made possible with the introduction of CloudKit sharing in iOS 10.
CloudKit Subscriptions
CloudKit subscriptions notify users when a change occurs within the cloud databases belonging to an installed app. Subscriptions use the standard iOS push notifications infrastructure and can be triggered based on various criteria, such as when records are added, updated, or deleted. Notifications can also be refined using predicates so that notifications are based on data in a record matching specific criteria. When a notification arrives, it is presented to the user in the same way as other notifications through an alert or a notification entry on the lock screen.
Obtaining iCloud User Information
Within the scope of an app’s cloud container, each user has a unique, app-specific iCloud user ID and a user info record where the user ID is used as the record ID for the user’s info record.
The record ID of the current user’s info record can be obtained via a call to the fetchUserRecordID(completionHandler:) method of the container instance. Once the record ID has been obtained, this can be used to fetch the user’s record from the cloud database:
container.fetchUserRecordID(completionHandler: { recordID, error in if let err = error { // Failed to get record ID } else { // Success – fetch the user's record here }
The record is of type CKRecordTypeUserRecord and is initially empty. However, once fetched, it can store data in the same way as any other CloudKit record.
CloudKit can also be used to perform user discovery. This allows the app to obtain an array of the users in the current user’s address book who have also used the app. For the user’s information to be provided, the user must have run the app and opted in to provide the information. User discovery is performed via a call to the discoverAllIdentities(completionHandler:) method of the container instance.
The discovered data is provided in the form of an array of CKApplicationUserInfo objects which contain the user’s iCloud ID, first name, and last name. The following code fragment, for example, performs a user discovery operation and outputs to the console the first and last names of any users that meet the requirements for discoverability:
container.discoverAllIdentities(completionHandler: ( {users, error in if let err = error { print("discovery failed %@", err.localizedDescription) } else { for userInfo in user { let userRecordID = userInfo.userRecordID print("First Name = %@", userInfo.firstName) print("Last Name = %@", userInfo.lastName) } } })
CloudKit Console
The CloudKit Dashboard is a web-based portal that provides an interface for managing the CloudKit options and storage for apps. The dashboard can be accessed via the https://icloud.developer.apple.com/dashboard/ URL or using the CloudKit Dashboard button located in the iCloud section of the Xcode Capabilities panel for a project, as shown in Figure 50-2:
Access to the dashboard requires a valid Apple developer login and password and, once loaded into a browser window, will appear, providing access to the CloudKit containers associated with your team account.
Once one or more containers have been created, the console provides the ability to view data, add, update, query, and delete records, modify the database schema, view subscriptions, and configure new security roles. It also provides an interface for migrating data from a development environment over to a production environment in preparation for an application to go live in the App Store
The Logs and Telemetry options provide an overview of CloudKit usage by the currently selected container, including operations performed per second, average data request size and error frequency, and log details of each transaction.
In the case of data access through the CloudKit Console, it is important to be aware that private user data cannot be accessed using the dashboard interface. Only data stored in the public and private databases belonging to the developer account used to log in to the console can be viewed and modified.
Summary
This chapter has covered a number of the key classes and elements that make up the data storage features of the CloudKit framework. Each app has its own cloud container, which, in turn, contains a single public cloud database in addition to one private database for each app user. Data is stored in databases in the form of records using key-value pair fields. Larger data, such as videos and photos, are stored as assets which, in turn, are stored as fields in records. Records stored in private databases can be grouped into record zones and may be associated with each other through the creation of relationships. Each app user has an iCloud user id and a corresponding user record, both of which can be obtained using the CloudKit framework. In addition, CloudKit user discovery can be used to obtain, subject to permission, a list of IDs for those users in the current user’s address book who have also installed and run the app.
Finally, the CloudKit Dashboard is a web-based portal that provides an interface for managing the CloudKit options and storage for apps.