Scorebook Sync Log 09 – CloudKit Xcoders Talk

Last night I gave my first Xcoders talk (and first tech talk in general), going over how I’ve brought CloudKit into Scorebook. The response was overwhelmingly positive from the group and I’m really happy about that. The talk had most all of the content that I’ve written so far in this series, but also went pretty in depth on CloudKit in general and has my upload and download processes as well as lots of code samples.

The slides can be found here.

Some notes:

  • I’m bummed that I haven’t shipped the Scorebook update yet, but I’m planning to in the next month or at the latest by the end of July.
  • I didn’t talk about the user switching things from my last post. Mostly because I haven’t finished the implementation and I didn’t want to talk about it in that state. I’ll keep writing about it here as I make progress though.

There was video recorded as well, and once that is posted to the Xcoders Vimeo account I’ll be sure to update this post with a link to it.

Update: Here is the video on Vimeo

Lastly, I love talking about this stuff. If you have questions hit me up on Twitter or via email.

Scorebook Sync Log 08 – User Switching

I’m feeling really confident in the things that I’ve put into sync in Scorebook. But now I’m starting to handle edge cases. Things like what happens when a user uses Scorebook without a current iCloud account, or how do I handle it when they log out and/or login again as a different user?

So I came up with a handy flowchart that I threw at Twitter and the Core Intuition Slack channel. Check it out:

Sync is hard.

I got some feedback on Slack from a guy who is doing something similar to what I am, but he’s accounting for the need to reset either the local store from the cloud or vice versa. The assumption is that at some point one or the other will become wrong and need to go back to square one. I may be overconfident here, but I’m leaning against doing that. My desired UI for syncing is a single switch on the settings screen. I think that’s how I’m going to launch, and I’ll have to see what the feedback is from users.

I’ve started writing the code to handle all of this and so far it’s not as bad as I thought it would be. I need to do a lot of testing still, though (and I’ve yet to start in on merging).

Do you see anything in the chart that might need to be changed? I’d love to hear about it!

Scorebook Sync Log 07 – Responding to Deletions

(Thanks to David Hoang for the encouragement to post this)

I thought I was close to being done, and then I remembered that I haven’t implemented responding to deletions from CloudKit locally. Here’s how the process works:

  1. Delete the object from Core Data & save the context
  2. Respond to the context did save notification and send CloudKit CKRecordID objects representing the deleted record to CloudKit
  3. CloudKit will send a notification to all subscribed devices letting them know of the deletion, with the corresponding CKRecordID object.
  4. Figure out what entity that object belongs to, and delete the instance from the database
  5. Save the updated database.

Pretty easy, right? Except, consider this: there isn’t a way to know the recordType of the CKRecordID coming down. The recordType is the database equivalent of a table, and it’s a property on CKRecord, not CKRecordID. When I’m creating CKRecords from my managed objects, I set the recordType property to a string representing my entity. That keeps things clean (i.e. my SBGame instances would all have record types of SBGame).

I haven’t talked about uniquing objects in CloudKit yet, so let’s go back to the beginning there. When you create a CKRecord you can specify it’s unique ID (made up of a recordName property which is a string, and a zone if it’s going to a custom zone). Those 2 factors combine with the CKRecord’s recordType property to create a primary key. Good database practice so far. You can also choose not to specify a recordID at the CKRecord’s creation, and it will be created when saved to the server.

For ease, I have decided to create a unique ID as a UUID string that is set on -awakeFromInsert on all of my entities. That way I always know what their unique ID will be, and I don’t have to worry about saving them back to the database when they are saved to the cloud.

The first step to determining what record type the CKRecordID I’m responsible for deleting, I made a change to each entity’s -recordName property, so that it includes the entity name as the first part of the record name. Then it’s followed by a delimiter (I’m using “|~|”) and then the UUID. Here’s how that now looks:

- (void)awakeFromInsert
{
    [super awakeFromInsert];
    self.ckRecordName = [NSString stringWithFormat:@"SBGame|~|%@", [[NSUUID UUID] UUIDString]];
}

A sample recordName for an SBGame instance would be “SBGame||386c1919-5f25-4be2-975f-5b34506c51db”, with || being the delimiter between the entity name and the UUID. A bit hacky, but it works.

On the downloader I explode the string into an array using he same delimiter and grab the first object, which will be “SBGame”. Once I have the entity and the recordName to search for, I can put this in a fetch request to grab the object that needs deletion. Here’s what that looks like:

NSString *recordName = recordID.recordName;
NSString *recordType = [[recordName componentsSeparatedByString:@"|~|"] firstObject];

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"ckRecordName = %@", recordName];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:recordType];
fetchRequest.predicate = predicate;

NSArray *foundObjects = [backgroundContext executeFetchRequest:fetchRequest error:nil];
id foundObject = [foundObjects firstObject];
if (foundObject != nil) {
    [backgroundContext deleteObject:foundObject];
}

In my limited initial testing this is working well so far, and it’s a simple solution to the problem.

Scorebook Sync Log 06 – Fun with Protocols

In my usage of CloudKit I had to determine early on how I was going to upload batches of records and deal with batches coming down. I’m dealing entirely with CKRecord instances, so all of my entities will need to know how to handle those; both in how to create one from themselves, and how to turn turn one into itself.

I didn’t have any clever idea at first, so I punted. For saving records to CloudKit, I’m monitoring the NSManagedObjectContextDidSaveNotification (which isn’t verbose enough) and I get NSSets of managed objects. Here’s what my initial implementation looked like:

- (CKRecord *)ckRecordForManagedObject:(NSManagedObject *)managedObject
{
    CKRecordZoneID *userZone = [[SBSyncController sharedSyncController] userRecordZone];
    CKRecord *recordToReturn;

    if ([managedObject isKindOfClass:[SBPerson class]]) {
        recordToReturn = [(SBPerson *)managedObject cloudKitRecordInRecordZone:userZone];
    } else if ([managedObject isKindOfClass:[SBGame class]]) {
        recordToReturn = [(SBGame *)managedObject cloudKitRecordInRecordZone:userZone];
    } else if ([managedObject isKindOfClass:[SBMatch class]]) {
        recordToReturn = [(SBMatch *)managedObject cloudKitRecordInRecordZone:userZone];
    } else if ([managedObject isKindOfClass:[SBPlayer class]]) {
        recordToReturn = [(SBPlayer *)managedObject cloudKitRecordInRecordZone:userZone];
    } else if ([managedObject isKindOfClass:[SBScore class]]) {
        recordToReturn = [(SBScore *)managedObject cloudKitRecordInRecordZone:userZone];
    } else if ([managedObject isKindOfClass:[SBMatchImage class]]) {
        recordToReturn = [(SBMatchImage *)managedObject cloudKitRecordInRecordZone:userZone];
    } else if ([managedObject isKindOfClass:[SBMatchLocation class]]) {
        recordToReturn = [(SBMatchLocation *)managedObject cloudKitRecordInRecordZone:userZone];
    }

    return recordToReturn;
}

Yuck.

And similarly, I captured the -recordType property of an incoming CKRecord, then ran -isEqualToString: against it to determine which entity it represented. And then something interesting happened.

I got to the place where I was working through how to handle conflicts and needed a -modificationTimestamp property to get that done, and realized that I could use a protocol to declare uniform conformance across all of my entities. I could make sure that the class conforms to the protocol, and then set the property without having to do the ugliness of something like the above snippet.

And thus, SBCloudKitCompatible was born. This protocol defines 4 things in the interface:

@protocol SBCloudKitCompatible <NSObject>
@property (nonatomic, strong) NSString *ckRecordName;
@property (nonatomic, strong) NSDate *modificationDate;

- (CKRecord *)cloudKitRecordInRecordZone:(CKRecordZoneID *)zone;
+ (NSManagedObject *)managedObjectFromRecord:(CKRecord *)record context:(NSManagedObjectContext *)context;
@end

By conforming to this protocol, I’ve been able to cut out a bunch of code. Here’s what the snippet above now looks like:

- (CKRecord *)ckRecordForManagedObject:(NSManagedObject *)managedObject
{
    CKRecord *recordToReturn = nil;

    if ([managedObject conformsToProtocol:@protocol(SBCloudKitCompatible)]) {
        id<SBCloudKitCompatible> object = (id<SBCloudKitCompatible>)managedObject;
        recordToReturn = [object cloudKitRecordInRecordZone:self.zoneID];
    }

    return recordToReturn;
}

I think we can agree that it looks way, way better.

CocoaConf PDX 2015

I was blessed to go to Portland over the weekend for CocoaConf with my family in tow. We packed up the Hyundai Elantra and made the trek down last Thursday, completing our journey to the Embassy Suites PDX by mid-afternoon. The following 2 days were chock full of great talks, great conversation, and lots of motivation to go make great apps.

I went to great talks from Daniel Steinberg, Danny Greg, Laura Savino, and more. A few high points:

  • I had my mind expanded by Danny Greg's talk on MVVM. I want to start building my apps using this in mind in the future. I was unprepared for how awesome that architecture looks to wire things together on iOS. (This talk was actually voted the favorite of all the conference-goers)
  • I loved Daniel Steinberg’s talks. I come away from them wanting to play, learn, and teach. I can’t wait to expose my son to lots of things. Not just programming (don’t get me wrong, I will show him) but I want to also show him Christianity, math, English, science, sports. It’s going to be amazing to watch him grow and become his own person.
  • The Apple Watch talks were really good. David and Curt have spent lots and lots of time with the SDK and mockups thinking about how apps should work on that platform and they have been great to share with the broader community. I haven’t built anything for the watch but I now feel equipped to go try it out.

The conference was all-inclusive, even with the meals. Instead of going offsite to eat somewhere they provided lunch and dinner for us on Friday and lunch on Saturday. We got to sit together and mingle or work on something that has inspired us (I got started outlining my June Xcoders talk). The community amongst Cocoa developers is something that I’m truly growing to treasure more and more each day. I got to hang out with some folks that I’ve met up in Seattle but didn’t know that well, and I got to meet new people too.

I can’t say enough good things about CocoaConf. This is the second one I’ve been able to attend (after they visited Seattle last October) and both have been great experiences. The next time they’re up in the Northwest I’ll definitely be on the attendee list.

3 Days with Apple Watch

I placed my order for a Space Gray 42mm Apple Watch Sport at 12:03 PST, and was one of the fortunate ones to have it delivered on launch day last Friday. It came in the later afternoon and I’ve spent the last few days getting to know it. I’ve got a few thoughts that I hope will be helpful to those waiting for theirs to arrive or wondering what the experience is like.

Before I dive in, I want to couch my thoughts as very much initial impressions. This is a device that needs to be lived with. It’s definitely Apple’s most personal device yet (as they’ve been calling it) and I have only a few days with it. It’s also a 1.0. There are some things that work really well and some things that don’t; this is to be expected, and will only get better over time.

  • The build quality of the watch is outstanding. It’s slightly smaller even than the analog watch that I have been wearing for a few months now, and definitely lighter. The fluoroelastomer band really is comfortable. It’s soft and almost disappears when you are wearing it. I like the look of it as well. When I did my try on at the Apple Store, my wife commented that she liked the one I got the best. Vindication!

  • I was shocked when I looked this up the other day, but at 30 grams, the 42mm Sport watches are the same weight as a Jawbone Up. The Jawbone has no screen, yet Apple Watch has a wonderful retina display that enables worlds of other functions besides just fitness tracking.

  • The setup process is really great. There’s an information cloud that serves as a QR code for your phone to capture and then as the pairing continues, it narrows down into these cool shapes and finally an Apple logo with a circle around it. The circle fills up as your settings get moved over to the watch. Overall it took about 10 minutes or so to be up and running.

  • The first thing I tried was a speaker phone call to my wife (with our son also in the room so he could hear his first speakerphone calls). That was the first glimpse into the future for me. It’s so cool to finally have a Dick Tracy (or Michael Knight) watch finally on my wrist. I can see it being a big help in the car, but out in public might be a bit weird.

  • The fitness tracking works quite well, but there’s room for improvement. I really like having the goals on a daily basis. When you achieve them you get “awards” which are oddly fulfilling. I’ve set modest daily goals and have been able to hit my move and stand goals without problems the past couple of days. They can be adjusted, as well. The thing that I will probably have to look to an app for is weight training and tracking. The exercise app only supports cardio things (bike, treadmill, elliptical, etc). That’s not a huge deal for me.

  • The pedometer actually leaves something to be desired for me. It functions like a normal wrist-worn pedometer would, meaning that you need to swing your arms for steps to be counted (which wouldn’t work for Raquel Welch). I like to walk with my hands in my hoodie pockets, so the watch won’t tally those steps either. I have to be cognizant of how I’m walking in order to know my steps are being counted. What bums me out about that is my iPhone is most likely in my pocket, happily counting my steps. I wish the two would talk to each other. This feels like a low-hanging fruit feature for a future update.

  • I’ve installed a few apps, and am trying to keep that number as low as possible. So far my favorites are Overcast (which provides a great remote control feature to control playback) and MLB At-Bat. The At-Bat app offers a glance, so I can see the Mariners score easily from the watch face. I’m excited to see what developers will do with the Watch now that it’s available and we can use them.

  • The front Glance is always going to be the settings one. This is the equivalent of Control Center on iPhone, and it works really well. You can enable airplane mode, do not disturb, mute, and even ping your iPhone if you’ve misplaced it. The killer thing is you can mirror the settings from the watch to the phone. So if your phone enters do not disturb, so will your watch (and vice versa). It’s really nice.

Here are 2 tips that I hope you will find helpful:

  1. If you’re a heavy app user, then changing the “Activate on Wrist Raise” setting of “Resume To” so that the watch resumes where you were, rather than the watch face, could be very useful. I did this just after starting to use Overcast and it’s been great. I would like Apple to have a timeout on this, though. If I haven’t raised my wrist in 15 or 30 minutes, then go back to the watch face. That would be perfect I think.

  2. Go slow with notifications, glances, and apps. You can customize this all on your phone and it’s really easy to do. Don’t let yourself get overwhelmed with buzzing and tapping on your wrist. My default on the phone is to deny notifications to start so this might need to start on the phone for you, but on the watch you can allow the notifications to go through for any app you have them enabled on the phone. I like that touch a lot. Being picky about what buzzes your wrist will make for a more pleasant experience.

So those are a few thoughts and a couple tips. If you’ve got any questions you’d like me to answer, hit me up on Twitter or by email and I’ll try to post some follow-ups if there are any.

Portfolio

Hello

I love software development, especially for Apple's platforms. If I got to design my own position, it would be writing delightful software alongside a great team where we could elevate each other to do fantastic work together. If this sounds interesting to you, please get in touch!. You can also view my résumé here.

App Store

Skills

  • iOS
  • OS X
  • Objective-C
  • Swift
  • Cocoa
  • Cocoa Touch
  • Xcode
  • .NET
  • ASP.NET MVC
  • C#
  • Visual Studio
  • JavaScript
    • jQuery
    • node.js
  • PHP
  • SQL

Scorebook Sync Log 05 - Plan Accordingly

When I decided to begin sync implementation with CloudKit, I just started going. I watched the WWDC videos (mostly) and read through the getting started, then started coding.

That wasn't the best tactic. Turns out.

There are a couple of questions that I should have asked myself. Do I want to have record changes pushed to me automatically? Do I want atomic operations so that all of the items have to succeed?

(As a slight aside, take note that neither of those things can be done on the public database, this is only for private data)

Well, I do want at least the record changes. So I need to implement a custom record zone. This is done with a CKRecordZone object, and is kind of like the equivalent of a database schema. Instead of using the default zone (which can't have push notifications or atomic actions), I create my own like this:

CKRecordZoneID *zoneID = [[CKRecordZoneID alloc] initWithZoneName:@"ScorebookData" ownerName:CKOwnerDefaultName];
CKRecordZone *zone = [[CKRecordZone alloc] initWithZoneID:zoneID];

The zone name can be "ASCII characters and must not exceed 255 characters". The zone name needs to be unique within the user's private database (which makes using a single string as the zone name possible). The owner name can just have the string constant CKOwnerDefaultName, which I'm pretty sure gets changed server-side to be some sort of user identifier. The docs aren't super clear about that part, but the method notes say:

To specify the current user, use the CKOwnerDefaultName constant.

Poking inside that constant at runtime expands it to __defaultOwner__. CloudKit seems to know what to do, though.

The next part I had to figure out was how do I get records into this zone? You don't save anything to the zone object, but instead use a different initializer on the CKRecordID objects. So I added a paramteter to my -cloudKitRecord methods in my model categories to take a zone. Now the methods are - (CKRecord *)cloudKitRecordInRecordZone:(CKRecordZoneID *)zone, and the implememtation uses the -initWithRecordName:zoneID initializer to create the record ID.

The thing I'm going to talk about next time is setting this whole stack up. I'm coming to realize that CloudKit, like Core Data, is a stack. I'm building a sync controller (which is what injects the CKRecordZoneID into the model objects' zone parameter) and that is working well so far. Stay tuned.

Scorebook Sync Log 04 - Relationships

I want to finish out my schema bit with a little bit on relationships. CloudKit fully supports relationships so you can associate your data with one record type to another. However it’s not exactly like you would expect in a relational database. CloudKit supports 1:1 and 1:many relationships, but not many:many. That’s not a problem for me but it may be for some.

In designing these relationships, the big thing to know going in is that you create the reference from the foreign-key side, not the primary side. So if you have a product that is becoming a line item, where the left side might be the product with many line items, you create the relationship on the right side with the line item. See the hand-crafted image below for clarity.

Hand-crafted visual aid

Creating a relationship is straightforward, and you use a CKReference object to do so. The designated initializer is -initWithRecordID:action: and you can send in the record ID object of the record to reference. If you don’t have the record ID, you can send in the whole record to the -initWithRecord:action: method. I don’t know which is better, and if you don’t have a record ID then that’s the one to use. I was going to flip back and forth, but since I decided to make the record ID at creation time I stick with the initWithRecordID:action: method instead.

The action part of the initializer refers to delete rules. Do you want the child to be deleted when the parent goes away? If so you can pass in CKReferenceActionDeleteSelf, and if not then you’ll want to use CKReferenceActionNone. It’s pretty straightforward.

From there in my CloudKit record creation method, the reference becomes just another key on the CKRecord object. So it gets a key, with the reference as a value, and that’s it. It looks something like this:

CKRecord *record = [[CKRecord alloc] initWithRecordType:@“Player”];
CKRecordID *personID = [[CKRecordID alloc] initWithRecordName:self.person.ckRecordName;
CKReference *personReference = [[CKReference alloc] initWithRecordID:personID action:CKReferenceActionDeleteSelf];
record[@“person”] = personReference;

Save the record to a database and that’s all there is to it.

Scorebook Sync Log 03 - Schema Design

Building a schema on CloudKit is pretty straightforward. You get a CKRecord object and treat it like a dictionary. There are a few data types they can take (NSString, NSNumber, NSData to name a few) and it’s a key/value pair to get the data on the record. Once you have a record created, you save it to the database and your RecordType (i.e. a database table) is created. Simple. But there is a trick associated with related data.

One of the data types you can attach is a CKReference. This is how you glue 2 records together. CloudKit supports 1:Many relationships (not many:many though). The way you do the relating is through the child record, not the parent. This tripped me up a little bit because with my background in FileMaker I would typically go parent to child (like with a portal object on a layout).

Let me back up for a moment. I made a design decision early on to isolate my networking files as much as possible. It could be a hedge or just wanting to have all the syncing code in one place, it jut felt right. I put a new folder in my Model folder called Sync and in it I have categories on all my model objects called <objectName>+CloudKit.h/m. I want each entity to know how to turn itself into a CKRecord and how to turn a CKRecord into itself. That feels like the right way to go.

Inside each category I have a method called -cloudKitRecord that returns a CKRecord. Here’s a basic implementation:

- (CKRecord *)cloudKitRecord
{
    CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:self.ckRecordId];
    CKRecord *personRecord = [[CKRecord alloc] initWithRecordType:[SBPerson entityName]
                                                         recordID:recordID];

    personRecord[SBPersonFirstNameKEY] = self.firstName;
    personRecord[SBPersonLastNameKEY] = self.lastName;
    personRecord[SBPersonEmailAddressKEY] = self.emailAddress;

    if (self.imageURL) {
        CKAsset *imageAsset = [[CKAsset alloc] initWithFileURL:[self urlForImage]];
        personRecord[SBPersonAvatarKEY] = imageAsset;
    }

    return personRecord;
}

A few things to note here:

  • I’m overriding -awakeFromInsert on all of my managed objects to create a -ckRecordId string property (Just a random UUID) that I can use as my CloudKit record ID. This is the unique identifier across the system. I started out with something much more complicated but landed here for now.
  • I have used Accessorizer (MAS link) to create string constants for all of my managed object properties. See this talk from Paul Gorake for more Core Data amazingness.
  • You can see my new CKAsset code at work here. I’m creating a new image asset with the URL to the image on disk, then you just use the asset as a property on the CKRecord. CloudKit handles the rest, which is really nice.

On the flipside, I have methods in the category that will take a CKRecord and create the Core Data managed object out of it. I haven’t gotten to the downstream sync just yet (I just ran into the need for CKRecordZones that I’ll probably write about next). Once I start the downstream I’ll put up a post about it too.

I’m going to come back to the CKReference stuff in my next post, since this one got a bit too long. Stay tuned.