CloudKit Tutorial
This tutorial shows how to use the iOS CloudKit framework to query, insert and delete data in an iCloud database.
Tutorial Steps
-
To follow along with the video, download the starter project which has the basic UITableViewController setup but no CloudKit support:
-
In the target, configure your own bundle identifier:
-
Enable iCloud support for the app in the target under Capabilities:
-
Open the CloudKit Dashboard from the iCloud capability, choose the iCloud container for the app (make take a minute or two after enabling iCloud to appear) and choose Development » Data:
-
Make yourself familiar with the database types:
Private database: Requires iCloud sign-in, separate database for every iCloud account, you cannot see user data in production and are not responsible for privacy issues.
Shared database: Requires iCloud sign-in, data can be shared between users, security rules can be configured.
Public database: No iCloud sign-in required for read-only access to the data.
-
Create a record type Errand and add a field name:
-
Create an index for the Errand recordType to be able to query for the objects:
-
Import the CloudKit framework, a CloudKit database instance and store it in the ErrandsModel class:
import CloudKit import UIKit // ... class ErrandsModel { private let database = CKContainer.default().privateCloudDatabase // ... }
-
Extend the Errand struct to be a wrapper around a CKRecord object:
struct Errand { fileprivate static let recordType = "Errand" fileprivate static let keyName = "name" var record : CKRecord init(record : CKRecord) { self.record = record } init() { self.record = CKRecord(recordType: Errand.recordType) } var name : String { get { return self.record.value(forKey: Errand.keyName) as! String } set { self.record.setValue(newValue, forKey: Errand.keyName) } } }
-
The CloudKit API will notify its caller about finished operations on background threads. Extend the ErrandsModel so that it sends its notifications by default on the main queue:
class ErrandsModel { // ... var errands = [Errand]() { didSet { self.notificationQueue.addOperation { self.onChange?() } } } var onChange : (() -> Void)? var onError : ((Error) -> Void)? var notificationQueue = OperationQueue.main private func handle(error: Error) { self.notificationQueue.addOperation { self.onError?(error) } } // ... }
-
Implement the ErrandsModel.refresh method to query the records. Map the records to Errand values:
@objc func refresh() { let query = CKQuery(recordType: Errand.recordType, predicate: NSPredicate(value: true)) database.perform(query, inZoneWith: nil) { records, error in guard let records = records, error == nil else { self.handle(error: error!) return } self.errands = records.map { record in Errand(record: record) } } }
-
Implement the addErrand method to create a CKRecord object:
func addErrand(name : String) { var errand = Errand() errand.name = name database.save(errand.record) { _, error in guard error == nil else { self.handle(error: error!) return } } }
-
Run the app in the simulator. Sign in to iCloud from the preferences with a sandbox tester account (See the tutorial video at 18:30 on how to create such an account). Test to add an items - they should appear if you manually refresh the table view using the refresh control (dragging the view to the bottom).
-
Implement the delete method to remove records:
func delete(at index : Int) { let recordId = self.errands[index].record.recordID database.delete(withRecordID: recordId) { _, error in guard error == nil else { self.handle(error: error!) return } } }
-
CloudKit query results are not updated immediately after changes: Implement keeping insertions and deletions local until the objects are returned / not returned any more by the query:
class ErrandsModel { // ... var records = [CKRecord]() var insertedObjects = [Errand]() var deletedObjectIds = Set<CKRecordID>() func addErrand(name : String) { // ... self.insertedObjects.append(errand) self.updateErrands() } func delete(at index : Int) { // ... deletedObjectIds.insert(recordId) updateErrands() } private func updateErrands() { var knownIds = Set(records.map { $0.recordID }) // remove objects from our local list once we see them returned from the cloudkit storage self.insertedObjects.removeAll { errand in knownIds.contains(errand.record.recordID) } knownIds.formUnion(self.insertedObjects.map { $0.record.recordID }) // remove objects from our local list once we see them not being returned from storage anymore self.deletedObjectIds.formIntersection(knownIds) var errands = records.map { record in Errand(record: record) } errands.append(contentsOf: self.insertedObjects) errands.removeAll { errand in deletedObjectIds.contains(errand.record.recordID) } self.errands = errands debugPrint("Tracking local objects \(self.insertedObjects) \(self.deletedObjectIds)") } @objc func refresh() { let query = CKQuery(recordType: Errand.recordType, predicate: NSPredicate(value: true)) database.perform(query, inZoneWith: nil) { records, error in guard let records = records, error == nil else { self.handle(error: error!) return } self.records = records self.updateErrands() } } }