Refactoring NSCoding to Swift
Rewriting an old rusty Objective-C code is fun.
You start with shiny new classes, adding some optimisation and covering the rest with syntactic sugar.
But some parts can get tricky. Migration is one of them.
While fetching a String from UserDefaults
shouldn’t be a problem.
And CoreData
has a lightweight migration.
With good old NSCoding
, things might get tricky.
Let’s say we have an Objective-C Class LCToken
that conforms to NSCoding
Which used to be stored in UserDefaults
(storing sensitive data in UserDefaults
is bad for your security, kids)
How do we create a swift reincarnation without loosing any of the users data?
Let’s swift it up!
We might want to use Codable
protocol in our new class.
Most of the code is auto-generated for us. So the implementation is as simple as adding the protocol conformance.
Persistence with Codable
is also safe, and easy to set up
Testing
Let’s create a test to see if we can read the Legacy data.
Unfortunately, NSCoding
and Codable
are not compatible “out of the box”.
Migration
A simple solution to migrate a class from a legacy data is to make it NSCoding
conformant.
We will need to subclass NSObject
(I know, everything has it’s downsides)
and implement functions required by the protocol.
Now, if data fetching fails, we can try to decode it using NSCoding
.
Let’s run the test again…
There is one more thing we forgot to set up. Class names.NSKeyedArchiver
uses a classname as one of the keys for encoding.
Swift class names, though, are prefixed with a module name, so leaving it untouched wouldn’t work.
Luckily there is asetClass:forClassName:
method, which adds something called a class translation mapping for NSKeyedUnarchiver
NSKeyedUnarchiver.setClass(Token.self, forClassName: "LCToken")
Let’s add it in our code and run the test again.
It’s Green!
Our users can now update without loosing any of their data.
Migrate safely!
Disclamer
This example is oversimplified to keep focus on a main problem.
Author recommends not to use UserDefaults in UnitTests, but to have a dependency injected storage and mock it in tests.