IOS8 Cloudkit Tutorial - Part 2
<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.