NSKeyedUnarchiver with Swift 3
Swift 3 brought with it lots of breaking changes, making the process of upgrading from 2.x quite an undertaking.
Most of the changes are handled reasonably well by Xcode’s upgrade tool, but some changes cause issues not found until runtime, such as decoding basic types using NSKeyedUnarchiver
.
tl;dr: Attempt to decode from older versions or default to Swift 3.
Encoding & Decoding
If you’ve written iOS projects before, you probably know NSKeyedArchiver
and NSKeyedUnarchiver
well.
For those of you who don’t, these are the services used for storing data locally in a hierarchy of objects. (You can read more here.)
Swift 2.x
In older versions of Swift, you’d decode most things using decodeObjectForKey
:
let myOptionalString = coder.decodeObjectForKey("key1") as? String
let myOptionalInteger = coder.decodeObjectForKey("key2") as? Int
And if you were certain that the values were non-nil during the encode
step, you could unwrap them:
let myString = coder.decodeObjectForKey("key1") as! String
let myInteger = coder.decodeObjectForKey("key2") as! Int
Swift 3
One of the more significant changes to Swift 3 syntax is that it moved parameter names out of the method and into the parameter label.
For example, in this case, decodeObjectForKey("key")
is now called using decodeObject(forKey: "key")
.
Thus, the Xcode upgrade tool changed how to decode a required string to:
let myString = coder.decodeObject(forKey: "key1") as! String
And for basic types, eg Int
, Bool
, etc, it changed the method altogether to decode the specific type:
let myInteger = coder.decodeInteger(forKey: "key2")
Backwards Compatibility
Alright, so what’s the problem? The new syntax looks cleaner, is easier to understand, and according to Apple’s docs, things should default reasonably.
The problem is that the values encoded using Swift 2.x aren’t compatible.
So if you encoded a boolean using, say, Swift 2.3:
//swift 2.3
coder.encode(true, forKey: "myBooleanKey")
and then try to decode it using a build created with Swift 3:
//swift 3
let myBool = coder.decodeBool(forKey: "myBooleanKey")
it would raise a nasty exception:
EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
(To be clear, this would not cause any issues if you uninstalled the old build before installing the new, making it a problem likely not caught by the developer who is constantly rebuilding, or unit tests that persist only for a specific test case.)
The Solution
You can solve this by handling both cases: attempt to decodeObject
first, and then fall back to decoding the specific type, eg decodeBool
:
let myBool = coder.decodeObject(forKey: "myBooleanKey") as? Bool ?? coder.decodeBool(forKey: "myBooleanKey")
(In this case, if there is no value at key "myBooleanKey"
, decodeBool:forKey
will return false gracefully.)
You won’t need to do this for non basic types, such as strings or objects.