IOS8 Cloudkit Tutorial - Part 2

October 15, 2014
Shrikar Archak

<Image alt="IOS8 Cloudkit Tutorial - Part 2" objectFit="contain" src="/static/images/cloudkit2.png" height={350} width={1000} placeholder="blur" quality={100} />

This is the part 2 of the iOS 8 CloudKit tutorial series. If you haven’t read the part 1 I suggest you to take a look at Part1 before you move on to the next section

What we will cover

  • UI for ListViewController
  • NSPredicate
  • NSSortDescriptors
  • CKQuery
  • Protocol and Delegate/Delegation iOS (Our example CloudKitDelegate)
  • CKModifyRecordsOperation
  • Final Working Fetching Code

UI for ListViewController

This is how our story board looked at the end of part 1. For more information on layouts checkout these videos iOS Adaptive Layouts and iOS Autolayouts

<Image alt="IOS8 Cloudkit Tutorial - Part 2" objectFit="contain" src="/static/images/part2_1.png" height={350} width={1000} placeholder="blur" quality={100} />

Lets go to object library and drag a navigation controller from the library onto the main storyboard.

<Image alt="IOS8 Cloudkit Tutorial - Part 2" objectFit="contain" src="/static/images/part2_2.png" height={350} width={1000} placeholder="blur" quality={100} />

Select the view controller which was already existing. Goto Editor embed in navigation controller.

<Image alt="IOS8 Cloudkit Tutorial - Part 2" objectFit="contain" src="/static/images/part2_3.png" height={350} width={1000} placeholder="blur" quality={100} />

CTRL-DRAG from Root View Controller to the just embedded navigation view controller and select present modally.

<Image alt="IOS8 Cloudkit Tutorial - Part 2" objectFit="contain" src="/static/images/part2_5.png" height={350} width={1000} placeholder="blur" quality={100} />

Add bar buttons for Cancel and Done . Finally setup the IBActions and IBOutlets.

NSPredicate

NSPredicate is basically the matching criteria like id should be "123" or id should be "123" and count > 5 predicate can’t be nil if you want to fetch all records use the below mentioned predicate

let predicate = NSPredicate(value: true)

NSSortDescriptors

NSSortDescriptors defined the order in which the records are retrieved from the iCloud using cloudKit. Its take the key on which the sorting is supposed to be done along with the ordering like ascending:true or ascending:false. If there are more than one sort descriptors they can be passed as an array.

CKQuery

CKQuery is analogous to select query in RDBMS world. The query consists of 2 or more parts.

  • RecordType : What type of object to search for. In our example it is Todos but for other applications it can be a Post, Message etc.
  • Predicate : Predicates are the condition on which the records should be matched against
  • SortDescriptors : The order in which the keys should be returned. We provide the key and the order like ascending or descending.

Example of the above scenario

let predicate = NSPredicate(value: true)
let sort = NSSortDescriptor(key: "creationDate", ascending: false)

let query = CKQuery(recordType: "Todos",
    predicate:  predicate)
query.sortDescriptors = [sort]

Protocol and Delegate/Delegation iOS

Protocols : are similar to interfaces in OOP world.

Delegate : “A delegate is an object that acts on behalf of, or in coordination with, another object when that object encounters an event in a program.”

In CloudKit most of the operations are async, hence are very good candidates for the Delegation pattern. Delegating object will send a message or call a callback when certain events are completed. Its the responsibility of the delegate object to implement those protocols and handle callbacks generated by the delegating object.

protocol CloudKitDelegate {
    func errorUpdating(error: NSError)
    func modelUpdated()
}

In our case our viewcontroller will handle this protocol and take appropriate actions when CloudKit events are triggered

CKModifyOperation

I faced a wierd issue when I tried to display the todo entries after adding to iCloud. I was not able to fetch the last entry added. Documentation mentioned that all the operations are async and are run on the low priority threads and if I need to save something immediately I need to use CKModifyOperation. I tried but unfortunately didn’t work for me. If someone finds a solution to this please let me know in the comments.

This method saves the record with a low priority, which may cause the task to execute after higher-priority tasks. To save records more urgently, create a CKModifyRecordsOperation object with the desired priority. You can also use that operation object to save multiple records simultaneously.

Problem

CloudKit not returning the most recent data

let todoRecord = CKRecord(recordType: "Todos")
todoRecord.setValue(todo, forKey: "todotext")
publicDB.saveRecord(todoRecord, completionHandler: { (record, error) -> Void in
        NSLog("Saved in cloudkit")
        let predicate = NSPredicate(value: true)
        let query = CKQuery(recordType: "Todos",
            predicate:  predicate)

        self.publicDB.performQuery(query, inZoneWithID: nil) {
            results, error in
            if error != nil {
                dispatch_async(dispatch_get_main_queue()) {
                    self.delegate?.errorUpdating(error)
                    return
                }
            } else {
                NSLog("###### fetch after save : (results.count)")
                dispatch_async(dispatch_get_main_queue()) {
                    self.delegate?.modelUpdated()
                    return
                }
            }
        }

Result

Before saving in cloud kit : 3 Saved in cloudkit

Count after save : 3

WorkAround

Add it to the todos array on the client side.

What I tried

let ops = CKModifyRecordsOperation(recordsToSave: [todoRecord], recordIDsToDelete: nil)
   ops.savePolicy = CKRecordSavePolicy.AllKeys

   ops.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
       NSLog("Completed Save to cloud")

       let predicate = NSPredicate(value: true)
       let query = CKQuery(recordType: "Todos",
           predicate:  predicate)

       self.publicDB.performQuery(query, inZoneWithID: nil) {
           results, error in
           if error != nil {
               dispatch_async(dispatch_get_main_queue()) {
                   self.delegate?.errorUpdating(error)
                   return
               }
           } else {
               self.todos.removeAll()
               for record in results{

                   let todo = Todos(record: record as CKRecord, database: self.publicDB)
                   self.todos.append(todo)

               }
               NSLog("fetch after save : (self.todos.count)")
               dispatch_async(dispatch_get_main_queue()) {
                   self.delegate?.modelUpdated()
                   return
               }
           }
       }
   }
   publicDB.addOperation(ops)

Final Working Fetching Code

import Foundation
import CloudKit

protocol CloudKitDelegate {
    func errorUpdating(error: NSError)
    func modelUpdated()
}

class CloudKitHelper {
    var container : CKContainer
    var publicDB : CKDatabase
    let privateDB : CKDatabase
    var delegate : CloudKitDelegate?
    var todos = [Todos]()

    class func sharedInstance() -> CloudKitHelper {
        return cloudKitHelper
    }

    init() {
        container = CKContainer.defaultContainer()
        publicDB = container.publicCloudDatabase
        privateDB = container.privateCloudDatabase
    }

    func saveRecord(todo : NSString) {
        let todoRecord = CKRecord(recordType: "Todos")
        todoRecord.setValue(todo, forKey: "todotext")
        publicDB.saveRecord(todoRecord, completionHandler: { (record, error) -> Void in
            NSLog("Before saving in cloud kit : (self.todos.count)")
            NSLog("Saved in cloudkit")
            self.fetchTodos(record)
        })

    }

    func fetchTodos(insertedRecord: CKRecord?) {
        let predicate = NSPredicate(value: true)
        let sort = NSSortDescriptor(key: "creationDate", ascending: false)

        let query = CKQuery(recordType: "Todos",
            predicate:  predicate)
        query.sortDescriptors = [sort]
        publicDB.performQuery(query, inZoneWithID: nil) {
            results, error in
            if error != nil {
                dispatch_async(dispatch_get_main_queue()) {
                    self.delegate?.errorUpdating(error)
                    return
                }
            } else {
                self.todos.removeAll()
                for record in results{
                    let todo = Todos(record: record as CKRecord, database: self.publicDB)
                    self.todos.append(todo)
                }
                if let tmp = insertedRecord {
                    let todo = Todos(record: insertedRecord! as CKRecord, database: self.publicDB)
                    /* Work around at the latest entry at index 0 */
                    self.todos.insert(todo, atIndex: 0)
                }
                NSLog("fetch after save : (self.todos.count)")
                dispatch_async(dispatch_get_main_queue()) {
                    self.delegate?.modelUpdated()
                    return
                }
            }
        }
    }
}
let cloudKitHelper = CloudKitHelper()

If you have any questions/comments do comment on the post.

Github Repo : CloudKit

Subscribe to the newsletter

Get notified when new content or topic is released.

You won't receive any spam! ✌️