CloudKit Tutorial

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

CloudKit Tutorial

Last update: July 9, 2018 | Tested with: Xcode 10

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 to 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 a record Type
  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 keys = (name : "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.keys.name) as! String
            }
            set {
                self.record.setValue(newValue, forKey: Errand.keys.name)
            }
        }
    
    }
  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

Btn download 3c20f11b8e Download the finished example project
Btn read 3c0e607615 iOS Developer Blog
Btn subscribe 930758687e Subscribe: Email · Twitter
Btn training bbbdf557d2 Next iOS training: 25. Februar - 01. März 2019, Stuttgart
Btn about 5378472193 About me · Contact