RT

Storing Enums in UserDefaults

Context

NSUserDefaults is one of the core classes of OpenStep that have been there since its initial specification.

NSUserDefaults was designed and implemented around NSMutableDictionary. Because of this, C scalar types such as int, float, bool, and double, have to be boxed/unboxed into NSNumber instances behind the scenes. As NSDictionary can only hold objects as values.

Status Quo

Currently, if we want to store an Enum value in UserDefaults, the easiest way is to make the Enum conform to RawRepresentable and save the raw value of the member we want to store.

enum DistanceUnit: String {
    case automatic
    case metric
    case imperial
}
class Preferences {

    let userDefaults: UserDefaults = .standard

    var distanceUnit: DistanceUnit {
        set {
            userDefaults.setValue(newValue.rawValue, forKey: "distance-unit")
        }
        get {
            guard let rawValue = userDefaults.value(forKey: "distance-unit") as? String else {
                return .automatic
            }

            return DistanceUnit(rawValue: rawValue) ?? .automatic
        }
    }

}

As you can see, this can get a little bit too verbose the more Enum-backed preferences you store.

A Better Approach

Luckily, Swift has support for extensions and generics. We can leverage this to extend UserDefaults and add support for storing and retrieving values that conform to RawRepresentable.

// UserDefaults+RawRepresentable.swift
import Foundation

extension UserDefaults {

    func value<T: RawRepresentable>(_ type: T.Type, forKey key: String) -> T? {
        guard let value = value(forKey: key) as? T.RawValue else {
            return nil
        }

        return T(rawValue: value)
    }

    func value<T: RawRepresentable>(_ type: T.Type, forKey key: String, default: T) -> T {
        return value(type, forKey: key) ?? `default`
    }

    func setValue<T: RawRepresentable>(_ value: T, forKey key: String) {
        setValue(value.rawValue, forKey: key)
    }

}
class Preferences {

    let userDefaults: UserDefaults = .standard

    var distanceUnit: DistanceUnit {
        set {
            userDefaults.setValue(newValue, forKey: "distance-unit")
        }
        get {
            userDefaults.value(DistanceUnit.self, forKey: "distance-unit", default: .automatic)
        }
    }

}