Friday, June 5, 2015

More CloudKit

I had to take a break from my sync discoveries this week to respond to some customer support questions on two of my apps.  I submitted updates to both of them over the past few days.  As anyone knows who has done this, even getting app updates ready for the store can be hectic.  Anyway, I’m glad that’s over, now I just hope they make it through the review process soon, but with WWDC next week I’m thinking it will be a while.  Anyway, since the fixes were mostly "nice to haves" it’s no big worry.


Progress
So last week I walked through the initialization of my CloudKit DataService. This week the plan is to talk about the CRUD methods it exposes to the LocalDataService.


First let me talk about what is being stored.  The object being stored is called a LocalEntity.  It is very basic.  I have it extending a class called LocalPersistentObject.  My thinking is that any object I want to store in CloudKit will extend that class.  So I'll start with LocalPersistentObject:


1:  let CLOUD_STATUS_SHOULD_ADD:Int = 0  
2:  let CLOUD_STATUS_UP_TO_DATE:Int = 1  
3:  let CLOUD_STATUS_SHOULD_DELETE:Int = 2  
4:  let CLOUD_STATUS_SHOULD_UPDATE:Int = 3  
5:  let KEY_DATE_UPDATED = "DateUpdated"  
6:  let KEY_DATE_CREATED = "DateCreated"  
7:  let KEY_RECORD_NAME = "recordName"  
8:  let KEY_CLOUD_STATUS = "cloudStatus"  
9:  let KEY_TEMPORARY_ID = "temporaryID"  
10:  class LocalPersistentObject: NSObject {  
11:    var _cloudStatus = CLOUD_STATUS_SHOULD_ADD  
12:    var _tempID:String?  
13:    var dateUpdated:NSDate = NSDate()  
14:    var dateCreated:NSDate = NSDate()  
15:    var recordID:LocalRecordID?  
16:    override init() {  
17:      super.init()  
18:      self.tempID = "\(NSUUID().UUIDString)-Temp"  
19:    }  
The first part of the file shows the state flags I use to track the state of the object in the local storage.  I use this state to know what to do with the object when the client syncs.  This is stored in the _cloudStatus field.  I also have a _tempID field that is used to give the object a unique id when it is first created but not yet sync’d with the cloud.  This allows offline caching.


For this listing I took out the accessor methods for the tempID and cloudStatus fields just because they are not that interesting.  For local storage I overrode the initWithCoder and encodeWithCoder methods.  That’s how I am caching the objects locally.  As you will see though this really isn’t a requirement, it could be a NSManagedObject.  The design here is to separate the local persistent cache from the CloudKit implementation.  The key is the _cloudStatus field and the _tempID field.  You can think of LocalPersistentObject as being  an abstract class in other languages.  My intent is to never cache one of these locally.


The subclass, LocalEntity, is very simple and it represents the client's business object.  It only has two fields which are both strings, “name” and “desctext”.  The “desctext” holds text used to describe the entity in which I store a string describing when the object was last updated.  The “name” field is, well, the name of the Entity.  That literally is all there is to this object.  The “persistenty” stuff is all in the super class.


Now let’s go back and look at how these objects are used in the DataService CRUD methods.  I’ll start with the method to get all entites. This method is used when the client first connects to the CloudKit zone to get all the objects from the zone.  I expect this would only be called when the app is launched for the first time on a given device.

1:   // Retrieves all entities from CloudKit  
2:    func getEntities() {  
3:      // Build a predicate that is always true  
4:      let predicate = NSPredicate(format: "TRUEPREDICATE")  
5:      // Sort by the name field  
6:      let sortDescriptor = NSSortDescriptor(key: KEY_NAME, ascending: true)  
7:      // Set up the query  
8:      let query = CKQuery(recordType: "Entity", predicate: predicate)  
9:      query.sortDescriptors = [sortDescriptor]  
10:      // Perform the query on the correct zone  
11:      privateDB.performQuery(query, inZoneWithID: zoneID) {  
12:        (results, error) in  
13:          if (error != nil) {  
14:            println("error loading: \(error)")  
15:            // Notify the delegate an error occurred  
16:            if let delegate = self.delegate {  
17:              dispatch_async(dispatch_get_main_queue()) {  
18:                delegate.handleRemoteError(error)  
19:              }  
20:            }  
21:          }  
22:          else {  
23:            var entities = [LocalEntity]()  
24:            // Process the results we got and marshall the CKRecord objects into LocalEntity objects  
25:            for record in results {  
26:             let entity = EntityHelper.localEntity(record as! CKRecord)  
27:             entities.append(entity)  
28:            }  
29:            // Notify the delegate the entites were loaded.  
30:            if let delegate = self.delegate {  
31:              dispatch_async(dispatch_get_main_queue()) {  
32:                delegate.entitiesLoaded(entities)  
33:              }  
34:            }  
35:          }  
36:      }  
37:    }  

The code is pretty straight forward.  The only interesting part is line 26 where the results returned from the query are used to create an array of LocalEntity objects.  To help with this I created a EntityHelper class that can take a CKRecord and turn it into a LocalEntity.  It can also turn a LocalEntity into a CKRecord object.  I’ll go over that in a future post.


Here is the “addEntity” method:


1:  /**  
2:      Adds a LocalEntity to the CloudKit database  
3:      :param: entity The LocalEntity to be persisted.  
4:    */  
5:    func addEntity(entity:LocalEntity) {  
6:      // Get the record for the entity  
7:      let record = EntityHelper.recordForEntity(entity,zoneID: zoneID)  
8:      // Save the record  
9:      privateDB.saveRecord(record) {  
10:        record, error in  
11:        if (error != nil) {  
12:          // If there is an error notify the delegate  
13:          println("error saving entity: \(error)")  
14:          if let delegate = self.delegate {  
15:            dispatch_async(dispatch_get_main_queue()) {  
16:              delegate.handleRemoteError(error)  
17:            }  
18:          }  
19:        }  
20:        else {  
21:          // Convert the returned record from the save into a new LocalEntity  
22:          let entityToAdd = EntityHelper.localEntity(record as CKRecord)  
23:          // Set the tempID on the entity we just created to the tempID of the  
24:          // entity we had when we began the save. That way the delegate can  
25:          // update the correct entity in the local cache.  
26:          entityToAdd.tempID = entity.tempID  
27:          if let delegate = self.delegate {  
28:            dispatch_async(dispatch_get_main_queue()) {  
29:              delegate.entityAdded(entityToAdd)  
30:            }  
31:          }  
32:        }  
33:        // Pull down any other changes that might have occurred  
34:        DataService.sharedInstance.getDeltaDownloads()  
35:      }  
36:    }  

Essentially all this code does is convert the passed in LocalEntity into a CKRecord and save it to the CloudKit database.  One interesting part starts on line 23.  When the client first created the LocalEntity object it attached a temporary id, held in tempID, to the LocalEntity.  After the save we still have access to that original LocalEntity object, so we get the original tempID and place it on the record that was just saved to the cloud.


At this point we have a LocalEntity object that represents a successfully stored CKRecord.  It has a CKRecordID and a tempID.  Why?  

Remember the design, the client code only talks to the LocalDataService, so before the LocalDataService told the DataService to store the LocalEntity in the cloud it first persisted it into it’s local cache.  

But it needed an id so it gave it this temporary one. Now on the successful return trip from the CloudKit database it uses the tempID to find the LocalEntity in it’s local cache and update it.  

I’ll go over how the LocalDataService works in a future post, but suffice it to say, for a newly added object that has been successfully stored in CloudKit, the LocalDataService, after finding it in the local cache, updates the locally cached object’s data, marks its state as successfully stored in the cloud and removes the tempID as it is not needed for subsequent updates.  We can use the real recordID from here on out.


Next time I'll finish up the CRUD methods (Update and Delete)


Reading
I finished reading “Spring In Action” from Manning Publishing.  My overall opinion of the book is, if you want to learn Spring, here is where you go.  I always got the feeling the book wasn’t quite up-to-date for Spring 4.0 (although it said it was).  It was hard to tell when the author was discussing a Spring 4.0 only feature.  But overall I really liked the book and thought it was well put together and logically arranged.


News

Someone did post to my question on the CoreData forums about CoreData sync loosing sync records.  Basically they said “I’m having the same problem”. I'm rolling my eyes now.  A lot of good that did to help solve the problem.  Anyway, WWDC is next week, I’m not going, but I suspect there will be just too much goodness to consume that I’ll skip writing a post next week and bask in the holiday us geeks call WWDC Week!

No comments:

Post a Comment