CloudKit Tutorial

by @ralfebert · published July 09, 2018

This tutorial shows how to use the iOS CloudKit framework to query, insert and delete data in an iCloud database.

Tutorial Steps

  1. To follow along with the video, download the starter project which has the basic UITableViewController setup but no CloudKit support:

    » Errands-starter.zip

  2. In the target, configure your own bundle identifier:

    Bundle Identifier
  3. Enable iCloud support for the app in the target under Capabilities:

    Enable Cloudkit
  4. 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:

    iCloud Dashboard
  5. Make yourself familiar with the database types:

    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.

  6. Create a record type Errand and add a field name:

    Create a record Type
  7. Create an index for the Errand recordType to be able to query for the objects:

    Create an index
  8. 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
    
        // ...
    
    }
    
  9. 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)
            }
        }
    
    }
    
  10. 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)
        }
    }
    
        // ...
    
    }
    
  11. 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) }
        }
    }
    
  12. 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
            }
        }
    }
    
  13. 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).

  14. 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
            }
        }
    }
    
  15. 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()
            }
    
        }
    
    }
    

More information