Monday, May 25, 2015

Got It - Twice!!

Today is Memorial Day in the United States, and I just want to say THANK YOU, THANK YOU, THANK YOU, to all who have given everything for this country.  


Every year this time comes around, I try to think of ways to express to my friends what this day means.  I don’t think I do a very good job.  Having served for ten years in the Air Force myself, I worked with all types of people, but when it came down to “go time”, putting aside all personal desires, and working together as a team to accomplish the mission, there couldn’t have been a better group of people.  Again, I fail to do justice explaining what our folks in the  military go through an a daily basis.  But suffice it to say without them the way of life that we live each day would not exist and especially on this day I/we remember those who gave it all.  I fail to come up with words to express how grateful I am.   


Progress
I started this week with the goal of getting one of my persistent+sync test apps working.  It turned out I ended up getting two working (CloudKit and Ensembles). My plan in these blogs is to walk through the code of each.  This may take a while:  I’ll start first with the CloudKit solution as it was the first one I got working (although the Ensembles solution may be more interesting and fruitful in the end since that is probably what I will go with). I essentially used the design I talked about in my last post.


Here is how it works:


In the app delegate didFinishLaunchingWithOptions I added this code:


 let notificationSettings = UIUserNotificationSettings(forTypes: UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound , categories: nil)  
 application.registerUserNotificationSettings(notificationSettings)  
 application.registerForRemoteNotifications()  

This is boilerplate code to register for remote push notifications


I also implemented the application didReceiveRemoteNotifications callback method (the one without a completion handler) Here is the guts of that implementation:



     println("received remote notification: \(UIDevice.currentDevice().name)")  
     let note = CKNotification(fromRemoteNotificationDictionary: userInfo)  
     DataService.sharedInstance.handleNotification(note)  


Unlike other examples I found on the net, my solution uses custom zones, so I don’t need to get CKQueryNotifications, and rather can just take the userInfo as a simple CKNotification.  It gets passed to my DataService class that will do the sync’ing to the local store. Since I am using a custom zone the notification will be a ZoneChanged notification so I don’t need to setup a query.


If you remember from my last post I have the DataService which deals with CloudKit “stuff” and the LocalDataService which deals with the local client’s persistent store.  The client always uses the LocalDataService for its data needs. It never talks to the DataService, only the LocalDataService talks to it. So there is a basic assumption that the LocalDataService represents the most up-to-date data that the client has access to.  


So what does the DataService look like?  One thing I ran into early was the DataService is more closely connected to the data objects that it is syncing which is something I don’t like.  I’m still contemplating how to refactor this code so that it can be reused.


Another thing I realized was the DataService needs to have a way to tell the LocalDataService when things have changed in the cloud.  I chose to do this via a delegate pattern.  So when the LocalDataService stands up it registers as the delegate of the DataService.


Here is the DataService’s delegate protocol that the LocalDataService implments:



 protocol DataServiceDelegate {  
   func entitiesLoaded(entities: [LocalEntity])  
   func getLocalEntity(localRecordID: LocalRecordID) -> LocalEntity?  
   func entityAdded(entity: LocalEntity)  
   func entityUpdated(entity: LocalEntity)  
   func entityDeleted(localRecordID: LocalRecordID)  
   func hasEntityWithID(localRecordID: LocalRecordID) -> Bool  
   func handleRemoteError(error:NSError)  
   func databaseReset()  
 }  


A couple of things to note about this.  First, as I mentioned earlier,  you can see the DataService is very linked to the type of object’s it is persisting.  Also I chose to create a LocalEntity object.  This is the object that is used throughout the application. The DataService “marshals” the CKRecord’s it gets from the CloudKit database into and out of these objects.  The LocalDataService only uses these objects.  I’ll talk about this object structure in a future post.
  
Now onto a discussion of the DataService itself.


The DataService has the following instance fields:



   let container : CKContainer  
   let privateDB : CKDatabase  
   let subscriptionID = "subscription_entities"  
   let zoneID:CKRecordZoneID  
   var subscribed = false  
   var delegate: DataServiceDelegate?  


As you can see the DataService holds a reference to the CKContainer, the privateDB (as the records will not be shared among users), the zoneID and of course the delegate. Notice the subscriptionID is just a String.  I struggled with what this was supposed to look like (reverse DNS, UUID, etc).  It turns out it just needs to be a constant string that all clients using the app will use.


The subscribe field is initially set to “false” when the DataService is first initialized and then set to “true” when everything is setup.  This will be used to toggle sync’ing on and off and shortcut various DataService calls when sync’ing is not desired.


The DataService’s init method is as follows:



   override init() {  
     // Get the default container  
     container = CKContainer.defaultContainer()  
     // grab the private database  
     privateDB = container.privateCloudDatabase  
     // Create the zoneID  
     zoneID = CKRecordZoneID(zoneName: ZONE_NAME, ownerName: CKOwnerDefaultName)  
     // Call the super init  
     super.init()  
     // subscribe to the zone  
     connectToZone()  
   }  


This is a pretty standard init method.  Essentially I get the default container, and then it’s privateDB.  This is where I will store the user’s records.  Next I create a zoneID as I will be using a custom zone to store the records in.    


The only tricky part here is the ZONE_NAME constant.  This turns out to be essentially the default container name with the name of the zone tacked onto it.  So if your reverse DNS name was “com.companyA” and your app name was “myapp” and the zone was going to hold Entity objects then using this scheme the ZONE_NAME becomes “iCloud.com.companyA.myapp.entities”. I think it has to be prepended with “iCloud” as I have shown here since that is the default container id you will get when you turn on the iCloud capability.


The last part of initialization is connecting to the Zone.  Here is the connectToZone() method:



   private func connectToZone() {  
     // Fetch the record zone with the zoneID we are interested in  
     privateDB.fetchRecordZoneWithID(zoneID) {  
       recordZone, error in  
       if (error != nil) {  
         println("ERROR -- fetchRecordZoneWithID error code was: \(error.code)")  
         if (error.code == CKErrorCode.ZoneNotFound.rawValue) {  
           // Zone Not Found so we need to create it  
           dispatch_async(dispatch_get_main_queue()) {  
             self.createZone()  
           }  
         }  
       }  
       else {  
         println("zoneID exists")  
         dispatch_async(dispatch_get_main_queue()) {  
           self.subscribe()  
         }  
       }  
     }  
   }  


In this code I first attempt to fetch the zone I want to be subscribed to.  I am doing this to determine if the zone needs to be created.  It turns out that you only need to save the subscription to the container once.  I really couldn’t find anywhere on the net that explicitly said that, but through trial and error I found that was the case.  


If there was an error fetching the zone I look at the error code and if it is a ZoneNotFound error then I know I need to create the zone, if there isn’t an error then I still need to subscribe to the zone.  


This brings up another issue I ran into.  It turns out it is harder than I would like it to figure out what the error codes are.  When you get a CloudKit error and print it out all you see is a number.  Searching the net for CKError will give you the following link:

Direct link to CKError codes (Click the CKError link at the bottom of the page to open it)

You want to bookmark this you will be referring to it often.


Here is the createZone method:



   private func createZone() {  
     let zone = CKRecordZone(zoneName: ZONE_NAME)  
     privateDB.saveRecordZone(zone) {  
       recordZone, error in  
       if (error != nil) {  
         println("ERROR -- createZone error code was: \(error.code)")  
       }  
       else {  
         println("zone created!!")  
         dispatch_async(dispatch_get_main_queue()) {  
           self.subscribe()  
         }  
       }  
     }  
   }  


This is where we create the zone if it did not exist.  All I do here is created the zone and save it to the private database.  If no error occurs then I call the subscribe method which is what would be called if the previously listed connectToZone method found the zone already existed.


An important note here is that creating the zone and subscribing to the zone are one time actions that only need to be done by one of the user’s clients when connecting to CloudKit. Doing either of these a second time (say when the app is launched on a second device) results in an error being thrown. I don’t like seeing errors in my log so that is why this code is more complex than one would like.


On to the subscribe method:



   private func subscribe() {  
     if (subscribed) {  
       return  
     }  
     println("Subscribing to privateDB changes")  
     let subscription = CKSubscription(zoneID: zoneID, options: CKSubscriptionOptions.allZeros)  
     subscription.notificationInfo = CKNotificationInfo()  
     subscription.notificationInfo.alertBody = "notification from zone"  
     privateDB.saveSubscription(subscription) {  
       subscription, error in  
       if (error != nil) {  
         if (error.code == CKErrorCode.ServerRejectedRequest.rawValue) {  
           self.subscribed = true  
           println("already subscribed!")  
           self.listenForBecomeActive()  
           dispatch_async(dispatch_get_main_queue()) {  
             NSNotificationCenter.defaultCenter().postNotificationName(EVENT_SUBSCRIBED, object: nil)  
           }  
         }  
         else {  
           println("ERROR -- error subscribing: \(error.code)")  
           dispatch_async(dispatch_get_main_queue()) {  
             NSNotificationCenter.defaultCenter().postNotificationName(ERROR_SUBSCRIBING, object: nil)  
           }  
         }  
       }  
       else {  
         self.subscribed = true  
         println("subscribed!")  
         self.listenForBecomeActive()  
         dispatch_async(dispatch_get_main_queue()) {  
           NSNotificationCenter.defaultCenter().postNotificationName(EVENT_SUBSCRIBED, object: nil)  
         }  
       }  
     }  
   }  


First I check to see if the subscribed flag is set to “true” and if so bail as there is no need to run this code a second time.  


Next I create the subscription.  Since this is a custom zone subscription, it is easier to setup as I am interested in all changes to the zone.  I also add a CKNotificationInfo() object to the notificationInfo field of the subscription.  Even though Apple’s documentation for CKSubscription says that if you do not set the notificationInfo field push notifications will still be sent, I found that was not true.  One downside of doing this I found was that if the application is in the background and a push notification comes in the user will get a notification.  I’m not sure at this point how to “eat” these types of notifications.  I’ll have to look into that further.


Next the subscription is saved to the database.  If it gets rejected then I found that error code meant the subscription already existed so I set the subscribed flag to true and fire the notification that the DataService is now subscribed.  Otherwise I can’t deal with the error (more work is needed here) so I fire an event to say the subscription failed for some other reason and bail.


If saving the subscription worked then I do the same as I did if it got rejected.  At this point any listener to the EVENT_SUBSCRIBED event can now update its views.  I listen for this event in the first view controller that is stood up, so that it knows when to populate it’s tableView.


Whew!!, that was a lot, I think I’ll stop here for this week.  Next week I think I’ll go into some of the CRUD methods.


Reading
I’m one chapter from finishing reading Spring in Action.  I’m real excited about this last chapter as it is on Spring Boot and should springboard me into my next book “Spring Boot In Action”


News That Caught My Eye
Nothing of note this week.


Quick Looks
I actually dug hard into Ensembles documentation this week so that was my Quick Look effort but it turned out to be more of a project than a quick look.  I’ll have a post on this in the future.

No comments:

Post a Comment