In the hillarious and painfully accurate article Programming Sucks, Peter Welch said this:
Every programmer occasionally…opens up a file on their computer…This file is Good Code. It has sensible and consistent names for functions and variables. It’s concise. It doesn’t do anything obviously stupid. It has never had to live in the wild, or answer to a sales team…
Every programmer starts out writing some perfect little snowflake like this. Then they’re told on Friday they need to have six hundred snowflakes written by Tuesday, so they cheat a bit here and there and maybe copy a few snowflakes and try to stick them together or they have to ask a coworker to work on one who melts it and then all the programmers’ snowflakes get dumped together in some inscrutable shape…
The code in this article is not Good Code. In some places, it is in fact, Bad Code. There were opportunities to avoid this code, but sometimes sales teams and deadlines make Bad Code the Right Code for the problem. That’s why this article is titled “Real World NSCoding”.
I was recently brought in to help finish up a project that was essentially a large form. This wasn’t a simple sign up form however, this was a form that rivaled a small business tax preparation (and with all the fun liability implications too!). One of the features that hadn’t been implemented yet was the ability to save an incomplete form, and come back to it later by picking it from a list of forms.
Had the form been backed by something like a CoreData object, this would have been easy, but instead it was a class with hundreds of properties, and many of those properties were classes with hundreds of properties.
NSCoding To The Rescue
If you’re looking for an introduction to the subject, NSHipster’s article on NSCoding is a great place to start. For the unfamiliar, NSCoding is a protocol that defines only two methods:
initWithCoder:. NSCoding by itself isn’t that interesting, but paired with NSKeyedArchiver/NSKeyedUnarchiver you get a powerful and flexible way to persist arbitrary data objects to the file system.
My problem was that it would be too time consuming at this point to go back and manually implement
initWithCoder: on the dozens of classes used in the feature, and doing so would create a brittleness that would be hard to detect. I wanted to avoid situations where a developer added an NSString property to class, only to find out later that it needed to also be added to the NSCoding methods in order to be persisted.
Fortunately, the dynamic nature of the Objective-C runtime gave me some introspection tools that I could leverage to make things relatively painless.
I’ve had this method kicking around for a while (origins unknown). It’s in a category for NSObject. Don’t forget to
This give us a nice NSDictionary representation of any NSObject. I made another category method that levies this dictionary to encode the object:
Then for each class I need to persist, I simply declare that they conform to the
NSCoding protocol and add the following method:
While there are other more “magical” ways to accomplish this (overriding methods, swizzling, custom protocols), sometimes a little bit of boilerplate makes it easer for developers down the line to follow what’s happening.
The Code The Project Needs, But Not The One It Deserves
So far so good, right? Get a dictionary. Encode it. Easy. Now we just need to decode it.
Here’s where it get’s a little gross. Here’s the full, production method for you to take in (note that
VLog is a macro that does some fun things with
NSLog if DEBUG is defined). From that same NSObject category:
Then in any class we’ve previously encoded:
The logging is a little verbose, but it was helpful during development (and the macros ensure they won’t slow things down in production).
There’s a few downsides to this approach. While it’s mostly stable, we did run into crashes if a non
NSCoding class sat inside a foundation class (like
NSDictionary). This mostly wasn’t a problem, since the crashes were all the result of something we missed, and a crash made it easy to fix during production (instead of silently failing).
The other downside is this bit of code:
This is checking if it’s a readonly property by looking for the selector
setValue:. This is less than ideal, especially since you can declare custom setters for properties. However, I’ve never found a place where this is used in production code.
The last downside to this is side effects from calling
setValue: type selectors. Many examples you see of NSCoding involve setting the instance variables inside of
initWithCoder:, like so:
We had a great time trying to figure out why our final values didn’t match what we encoded, until we finally realized that some of the setter methods were setting other values in the class! That’s where the somewhat maniacal logging came into play. We ended up solving this by using a variant of this approach:
Any of our setter methods that had cascading effects all checked for
_doNotUpdate before doing any changes.
I won’t go over
NSKeyedUnarchiver here. Those classes are very straightforward once we had all the encoding/decoding worked out.
We always strive for Good Code. Clean Code. Code that conforms to all the standards and conventions and unicorns out there. But that doesn’t always work out. Sometimes the Right Code is very dirty indeed. So don’t feel bad when you end up committing atrocities in the name of deadlines. We’ve all done it. Some of us even blog about it.Share