Read Part 1 in series on Developing for CloudKit
Read Part 2 in series on Developing for CloudKit
In the 6 weeks since my last post on CloudKit Development, I’ve added the ability to make changes to a CKRecord and have those changes synced between devices. In this post, I'll cover syncing changes and data considerations.
The CKRecord from the previous posts had an image property that never changed after upload to CloudKit. Since then, I’ve added 2 properties that will need their changes synced. The first is a boolean, the second is an array.
I began by making the fatal assumption about CKRecord’s recordChangeTag property. I believed I could use it to query the server for CKRecords with changes and download changes on the result - turns out I was way off the mark.
/* Change tags are updated by the server to a unique value every time a record is modified. A different change tag necessarily means that the contents of the record are different. */ @property (nonatomic, readonly, copy) NSString *recordChangeTag;
That’s not what recordChangeTag was intended for so I had to look for another solution.
That solution was CloudKit’s Push Notifications. Basically, you register the app for notifications and the device will receive a notification each time CKRecord is updated (you can register for delete/creation notifs too).
That was great and all, but what do you do with that notification?
First, I don’t want to start a sync session in a background task. Downloading CKRecords in the background would be acceptable if they were small (<50KB) but they’re at least 3MB each. I also want to plan ahead and account for CKRecords with assets over 25MB each.
My goal from the start was to keep the sync engine as light-weight as possible, with minimal network transfers.
The other issue with background syncing is having to load the full sync engine as a background process, meaning far more overhead than necessary. Yes, these are artificial constraints I’ve placed on the project, and many apps on the App Store cram far more into a background task, but I digress.
The solution was to create a singleton class that keeps track of the CKRecordID.recordNames from the notification. I load the singleton, check to see if the recordID has been previously added, add it to an array if it hasn’t, then save to disk. The whole process is fast and lightweight. When the app resumes, I can download change to the records stored in the singleton, then clear the list.
Every decision comes with trade-offs. In a perfect world, I would sync changes in a background session. Of course it’s better for the user’s data to be up to date each time they open the app. But for the sake of battery life, mobile data usage, and overall performance of the OS, I think it’s the worth the user waiting 1-3 seconds for sync to complete. It’s a tradeoff I’m willing to make.
If you’ve read the previous 2 posts, updating the data for changes happens at the end of the sync session, after local offline changes are synced. In the spirit of doing the least amount of work possible, we sync inserts and deletions before changes. Consider this scenario - there are 5 total objects, 3 are on the client (synced previouslly), 2 new objects need to be downloaded from the server, one local object needs to be deleted, all objects have changes. If we synced the changes first, we would be updating the 3 local objects. Instead, we should complete the local deletion first, resulting in only updating the 2 remaining objects.
That didn’t explain the entire sync process but it illustrates the types of considerations you’ll have to make. Saving the user 5-10KB here and there doesn’t seem like much now, but after 1,000 syncs, it adds up.
What about insert/deletion notifications?
These are good for your typical CloudKit-based app where the data is online-only. For an app that allows the user to make offline changes, you need to handle this yourself.
CloudKit wasn't intended to sync Core Data. It was designed as a backend, a way for app developers to focus on native apps and let Apple handle the server implementation and cost. The sync engine I created is a small layer that sits between Core Data and CloudKit.
I'm happy with the results so far. The next step is to abstract the sync layer so it's independent of your data model. I hope to open source it at some point in 2015.
Update: Screenshot++ has shipped with the CloudKit sync engine. Give it a spin!