How NOT to set nil in UserDefaults

October 21, 2018

To clear a value from UserDefaults, I previously thought it was fine to do something like this:

UserDefaults.standard.set(nil, forKey: "myKey")

Turns out that this works differently in different iOS versions. Starting in iOS 11, setting nil for a key works as I expected and the following prints nil:

UserDefaults.standard.set(nil, forKey: "myKey")
print(UserDefaults.standard.data(forKey: "myKey"))
// nil

However, in iOS 10, UserDefaults actually tries to serialize nil into a Data:

UserDefaults.standard.set(nil, forKey: "myKey")
print(UserDefaults.standard.data(forKey: "myKey"))
// 135 bytes

If we look at this data, it seems like it might be a plist:

print(String(
    data: UserDefaults.standard.data(forKey: "myKey")!,
    encoding: .ascii
))
Optional("bplist00Ô
T$topX$objectsX$versionY$archiverÑTroot€¡	U$null† _NSKeyedArchiver(25:<>DI[")

Turns out that we can use PropertyListSerialization to actually print the serialized value of nil:

let data = UserDefaults.standard.data(forKey: "myKey")!
let propertyList = try! PropertyListSerialization.propertyList(
   from: data,
   options: [],
   format: nil
)
print(propertyList)
{
    "$archiver" = NSKeyedArchiver;
    "$objects" =     (
        "$null"
    );
    "$top" =     {
        root = "<CFKeyedArchiverUID 0x79e7c420 [0xf126e8]>{value = 0}";
    };
    "$version" = 100000;
}

For whatever reason, this was totally unexpected to me. It’s particularly tricky when you’re trying to interpret the value of a previously cleared UserDefaults key as JSON:

if let data = UserDefaults.standard.data(forKey: "myKey") {
    let json = try JSONDecoder().decode(MyClass.self, from: data)
    print(json)
} else {
    print("No data found")
}

This will work just fine on iOS 11+, but throw an error on iOS 10.

Here’s an example project which demonstrates this behavior. I ended up finding one stackoverflow post about it, but as far as I can tell this isn’t documented anywhere. If you have any more info about it, let me know 👋

Lesson learned — setting nil isn’t a good way to clear a UserDefaults value. Instead, just use the recommended approach:

UserDefaults.standard.removeObject(forKey: "myKey")