Playing with CloudKit

Catching up and CloudKit introduction

(Very) Long time, no see! I’ve been totally neglecting my blog, shame on me. I’ve been involved in a lot of projects, which sadly left me close to no time to produce content. I would like to focus on it more and write articles describing my freelance developer experience combined with small technical pieces, containing some frameworks that are either new or underused in my opinion within the development space.

The one I would like to start off with is CloudKit. CloudKit has been around since iOS 8 and is a framework which provides an interface for data management between Apple’s iCloud server and your application. Even though it’s been around for quite some time, I have rarely heard of anyone to be using it, mostly due to the existence of the more popular options like Firebase or Parse (RIP).

Advantages and disadvantages

I personally heard of CloudKit besides its presentation in the 2014 WWDC at a conference in 2015, where a usage example has been presented. However, ever since I have never considered using or trying it until I saw another WWDC video about what’s new in CloudKit. I always saw Firebase as the better alternative, providing more than CloudKit could. CloudKit has some advantages though, which I think are quite valuable and should definitely not be neglected. I will list them in a random order.

  1. No external dependencies needed – since CloudKit is part of Cocoa Touch, you do not need to load your app with external libraries.

     

  2. Authentication problem solved – CloudKit uses the user’s iCloud account hence you do not need to setup additional authentication within your app. All that’s necessary is that the user is logged in with their iCloud account on the device. Moreover, there are many users who do not want to trust unknown applications with their data, especially with their e-mail, so you give them the chance to use a platform that they already have confidence in.

  3. Push notifications – easy to create and use – compared to other solutions, integrating push notifications is very easy, without the need to create extra certificates and keeping track of the environment. You do not need to setup push notifications separately – once you enable CloudKit in Capabilities, it’s done. Also, you can generate notifications automatically and it’s incredibly easy. For example, imagine that you want to generate notifications each time an item is entered to your database. You can do that automatically with CloudKit – just create a CKQuerySubscription with the firesOnRecordCreation option and you are set!

  4. Different database types – private, shared, public – having different database types relieves you as a developer from the pain of storage issues. Users will save their data in their own iCloud account. The data you want to make public among all users will be accessible within the public database. A user can also share their data with other users by using a shared database.

  5. Ease of transitioning to production – you can simply press a button called “Deploy to production” and voila, the app has been prepared to work in production, no duplication needed.

  6. Powerful API – provides a lot of convenience functions and a very strong search. You can use NSPredicate and NSSortDescriptor objects when building up your queries, which makes it very easy to find and display data.

To me, these are the most important advantages compared to other solutions. However, I consider the following disadvantages:

  1. Single platform – you cannot use CloudKit for Android or other platform apps. * EDIT – Thanks to @oriyentel for pointing this out – you can actually use CloudKit from other platforms, via a JS library provided by Apple, however you will still need an iCloud account if you want to use the private/shared database features.

     

  2. Tracking and more advanced analytics – you have certain tracking mechanisms, such as database usage or database logs, but you cannot track custom events and generate usage data based upon that. You could for example create and save a database entry for a custom event, but that’s a workaround, that also does not give you enough user or usage information compared to other solutions, where you would have plotting options, graphs and such.

  3. Data is not accessible without an iCloud account in development – compared to other platforms, you cannot access data without an account in development. Why is this important? When testing in-app purchases, you cannot be logged in with another account. If you want to combine both, you might run into a bit of a problem.

Getting started

Screen Shot 2018-03-08 at 4.45.00 PM

The first thing to do is to enable iCloud in your app’s capabilities. As you can notice in the image, the Push Notifications were enabled automatically.

A container has been automatically created for the project. This container will be referenced in the CloudKit dashboard and represents the link between your application and the iCloud server.

Screen Shot 2018-03-08 at 4.50.12 PM

Here’s the two different boards – one for development, one for production. Once you’re done with implementation, all you need to do is press “Deploy to production” and you’re done.

This is all you need to do in order to setup a simple project. Let’s move to creating some records.

Simple CRUD

Screen Shot 2018-03-08 at 4.54.42 PM

I created a record type called SimpleCloudCitizen and will show you how to CRUD based on that object.

I wanted to include a UI, but code is more relevant, hence I removed it and will only display code and explain how to perform the CRUD operations.

First, we will create a SimpleCloudCitizen struct. What’s relevant to us is the age, country and name.

Screen Shot 2018-03-08 at 5.03.47 PM

So far, so good. Now, we need to add the code which performs CloudKit interactions. We will create a CloudKitManager struct, which will be a singleton which handles the CK operations. Since we’re performing CRUD on the private database, we need to think of the following 4 operations: create, read, update and delete – hence, 4 functions. However, we need to add a 5th function, which will check whether the current user has been logged in:

import Foundation 

import CloudKit 

typealias RequestCKPermissionCompletion = (_ accountStatus: CKAccountStatus, _ error: Error?) -> Void 

typealias CreateCitizenCompletion = (_ success: Bool, _ resultingSimpleCloudCitizen: SimpleCloudCitizen?, _ error: Error?) -> Void 

typealias RetrieveCitizensCompletion = (_ citizens: [SimpleCloudCitizen]?, _ error: Error?) -> Void 

typealias UpdateCitizenCompletion = (_ success: Bool, _ resultingSimpleCloudCitizen: SimpleCloudCitizen?, _ error: Error?) -> Void 

typealias DeleteCitizenCompletion = (_ success: Bool, _ error: Error?) -> Void 

struct CloudKitManager { 

    static let shared = CloudKitManager() 

    func requestCloudKitPermission(completion: @escaping RequestCKPermissionCompletion) {} 

    func createSimpleCloudCitizen(for citizen: SimpleCloudCitizen, completion: @escaping CreateCitizenCompletion) {} 

    func retrieveAllCitizens(completion: @escaping RetrieveCitizensCompletion) {} 

    func updateSimpleCloudCitizen(from citizenId: String, to citizen: SimpleCloudCitizen, completion: @escaping UpdateCitizenCompletion) {} 

    func deleteSimpleCloudCitizen(citizen: SimpleCloudCitizen, completion: @escaping DeleteCitizenCompletion) {} 

}

Time to fill up those functions.

To retrieve the account permissions, we will use the following request on the default CKContainer object:

func requestCloudKitPermission(completion: @escaping RequestCKPermissionCompletion) {
    CKContainer.default().accountStatus(completionHandler: completion)
}

This retrieves the status as a CKAccountStatus enum.

Next, to create a SimpleCloudCitizen, we need to convert the local struct to a CKRecord. Let’s write a simple function for that:

func toCKRecord() -> CKRecord {
    let record = CKRecord(recordType: "SimpleCloudCitizen") 
    record["name"] = name as NSString 
    record["age"] = NSNumber(integerLiteral: age) 
    record["country"] = country as NSString 
    return record 
}

It’s true, this will introduce some coupling between the model and the data layer, but we will take this simple approach for the purpose of our example. Now, we will access the privateDatabase property of the default container to perform an async save operation for the resulting record:

func createSimpleCloudCitizen(for citizen: SimpleCloudCitizen, completion: @escaping CreateCitizenCompletion) {
    let record = citizen.toCKRecord()
    
    CKContainer.default().privateCloudDatabase.save(record) { (serverRecord, error) in
        guard let _ = serverRecord else {
            DispatchQueue.main.async {
                completion(false, nil, error)
            }
            return
        }

        DispatchQueue.main.async {
            completion(true, citizen, nil)
        }
    }
}

Retrieval is a bit different. We must use an NSPredicate object. In this case, we will use the “TRUEPREDICATE”, meaning a predicate which does not impose any rule on the desired result set. Consequently, since we’re creating citizens from a server object, we should create a static function on the SimpleCloudCitizen struct:

static func fromCKRecord(record: CKRecord) -> SimpleCloudCitizen? {
    guard let name = record["name"] as? String,
          let ageNSNumber = record["age"] as? NSNumber,
          let country = record["country"] as? String else {
        return nil
    }

    return SimpleCloudCitizen(name: name, age: ageNSNumber.intValue, country: country)
}

This will create a citizen, if and only if the params exist in the server record, otherwise it will return nil. On that note, let’s finish our retrieval function:

func retrieveAllCitizens(completion: @escaping RetrieveCitizensCompletion) {
    let predicate = NSPredicate(format: "TRUEPREDICATE")
    let query = CKQuery(recordType: "SimpleCloudCitizen", predicate: predicate)

    CKContainer.default().privateCloudDatabase.perform(query, inZoneWith: nil) { (records, error) in
        guard let records = records else {
            completion(nil, error)
            return
        }
        
        let citizens = records.compactMap { SimpleCloudCitizen.fromCKRecord(record: $0) }
        completion(citizens, error)
    }
}

Updating is a bit different. We need some unique parameter to be able to identify entries within the database. We can use the recordName parameter (see the Record Type image above), which represents a unique identifier for the database entry for our citizen. We need to save it when retrieving it from the server, hence we need to update the SimpleCloudCitizen struct, by adding it another property, and also the function which creates the user from a CKRecord.

import Foundation
import CloudKit

struct SimpleCloudCitizen {
    let name: String
    let age: Int
    let country: String
    var citizenID: String? // Optional, because it can be nil when we create it from the client app
    
    func toCKRecord() -> CKRecord {
        let record = CKRecord(recordType: "SimpleCloudCitizen")
        record["name"] = name as NSString
        record["age"] = NSNumber(integerLiteral: age)
        record["country"] = country as NSString
        return record
    }
    
    static func fromCKRecord(record: CKRecord) -> SimpleCloudCitizen? {
        guard let name = record["name"] as? String,
              let ageNSNumber = record["age"] as? NSNumber,
              let country = record["country"] as? String else { return nil }
        
        let citizenID: String = record["recordID"]?.recordName
        
        return SimpleCloudCitizen(name: name, age: ageNSNumber.intValue, country: country, citizenID: citizenID)
    }
}

We also need to change the manager create function a bit – we need to update the object when the result comes from the server so that it is set with the correct id:

func createSimpleCloudCitizen(for citizen: SimpleCloudCitizen, completion: @escaping CreateCitizenCompletion) {
    let record = citizen.toCKRecord()
    
    CKContainer.default().privateCloudDatabase.save(record) { (serverRecord, error) in
        guard let serverRecord = serverRecord else {
            DispatchQueue.main.async {
                completion(false, nil, error)
            }
            return
        }
        
        DispatchQueue.main.async {
            completion(true, SimpleCloudCitizen.fromCKRecord(record: serverRecord), nil)
        }
    }
}

However, from a CloudKit point of view, an update is essentially a create – meaning that if there’s an already existing entry, it will update that, otherwise, it will create a new one. If we have the ID on our object, that means that there’s already a server entry, otherwise, it’s a new one. Hence we only need 1 function! Let’s remove the update function and change the toCKRecord function of the SimpleCloudCitizen, to make sure we add the record ID on it:

func toCKRecord() -> CKRecord {
    let record: CKRecord
    // Differentiate between existing and non-existing citizens (update vs create)
    if let citizenID = citizenID { // Update
        record = CKRecord(recordType: "SimpleCloudCitizen", recordID: CKRecordID(recordName: citizenID))
    }
    else { // Create
        record = CKRecord(recordType: "SimpleCloudCitizen")
    }
    
    record["name"] = name as NSString
    record["age"] = NSNumber(integerLiteral: age)
    record["country"] = country as NSString
    return record
}

Finally, deletion. The only thing we need to make sure is that we have a record ID to delete, otherwise, it means that the citizen was not even created on the server.

func deleteSimpleCloudCitizen(citizen: SimpleCloudCitizen, completion: @escaping DeleteCitizenCompletion) {
    guard let citizenID = citizen.citizenID else {
        completion(false, nil)
        return
    }
    
    CKContainer.default().privateCloudDatabase.delete(withRecordID: CKRecordID(recordName: citizenID)) { (id, error) in
        guard let _ = id else {
            completion(false, error)
            return
        }
        
        completion(true, nil)
    }
}

Final thoughts

That’s a summed up way to create a CloudKit manager and to perform CRUD operations with it. I believe it’s quite simple to use, the API is very powerful and you can do even more complicated operations (that are out of the scope of this article).

I hope that you will find this article informative and helpful. If you have any questions, misunderstandings or think that I did/said something wrong, please do not hesitate to contact me or to leave a comment. Thank you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: