iOS App Development iOS Swift Tutorial iOS8

IOS8 Cloudkit Tutorial – Part 2

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
images

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

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

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

images

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.

If you are interested in learning iOS Development I suggest you take a look at Building an app every month in 2015

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()

Learn how to build a complete Trivia App

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

Github Repo : CloudKit

About the author

Shrikar

Backend/Infrastructure Engineer by Day. iOS Developer for the rest of the time.

  • Muhammad Riaz

    Hi Aaron,
    edit following line
    let sort = NSSortDescriptor(key: “creationDate”, ascending: false)
    and change it to:
    let sort = NSSortDescriptor(key: “todotext”, ascending: true)

  • RC

    Shrikar
    I am new to ios development, I have developed an app for our remote users but it needs to get the data from our ERP systems and also update the data back. I am unsure what is the best way to do this (icloud, parse?)
    Any advice on how to accomplish this would be much appreciated.

  • prwiley

    Can someone please explain this line of code:

    var todos = [Todos]()

    It seems to appear without explanation. It throws an error in x-code 7.1 beta

    • Yunus Nedim Mehel

      The tutorial does not explain everything completely. The code won’t work because you do not have an object Todos. Download the project, finish the tutorial from there.

  • Yunus Nedim Mehel

    Download the project code if needed