Key Value Coding in Swift

I was having a discussion on Twitter with someone about KVC and creating a typed version. Well, I do not think we can create a fully type-safe version, at least not at compile time. However, we should be able to come close at runtime, I think.

Anyway, here is a stab at an implementation.

The basic protocol is simple, just setValue() and getValue() functions:

protocol KeyValueCodable : _KeyValueCodable {
    mutating func setValue<T>(value: T, forKey: String) throws
    func getValue<T>(forKey: String) throws -> T
}

However, some implementation details are necessary to support a default implementation via protocol extensions:

protocol _KeyValueCodable {
    static var _codables: [KVCTypeInfo] { get }
    var _kvcstore: Dictionary<String, Any> { get set }
}

struct KVCTypeInfo : Hashable {
    let key: String
    let type: Any.Type

    // Terrible hash value, just FYI.
    var hashValue: Int { return key.hashValue &* 3 }
}

func ==(lhs: KVCTypeInfo, rhs: KVCTypeInfo) -> Bool {
    return lhs.key == rhs.key && lhs.type == rhs.type
}

As you might be gathering, the basic premise is to use a backing store to maintain our values and type information. The implementation can then verify that the data coming in is correct.

extension KeyValueCodable {
    mutating func setValue<T>(value: T, forKey: String) {
        for codable in Self._codables {
            if codable.key == forKey {
                if value.dynamicType != codable.type {
                    fatalError("The stored type information does not match the given type.")
                }

                _kvcstore[forKey] = value
                return
            }
        }

        fatalError("Unable to set the value for key: \(forKey).")
    }

    func getValue<T>(forKey: String) -> T {
        guard let stored = _kvcstore[forKey] else {
            fatalError("The property is not set; default values are not supported.")
        }

        guard let value = stored as? T else {
            fatalError("The stored value does not match the expected type.")
        }

        return value
    }
}

Of course, the errors could be more meaningful, but I'll leave that as an excercise for the reader.

Initially I looked at using throws to capture the error. However, the usage of the code becomes quite annoying. Also, there is a fairly big limitation as computed properties (which is what we'll need for the next section), do not support throwing (http://www.openradar.me/21820924).

Ok, finally, let's implement this in a type:

struct Person : KeyValueCodable {
    static var _codables: [KVCTypeInfo] { return [ _idKey, _fnameKey ]}
    var _kvcstore: Dictionary<String, Any> = [:]
}

extension Person {
    private static let _idKey = KVCTypeInfo(key: "id", type: Int.self)
    private static let _fnameKey = KVCTypeInfo(key: "fname", type: String.self)

    init(id: Int, fname: String) {
        self.id = id
        self.fname = fname
    }

    var id: Int {
        get { return getValue("id") as Int }
        set { setValue(newValue, forKey: "id") }
    }

    var fname: String {
        get { return getValue("fname") as String }
        set { setValue(newValue, forKey: "fname") }
    }
}

All of the stored properties are put into the non-extended type. If we're using classes, this could live happily in a base class. The extension contains all of the meat, and unfortunately, all the boiler-plate code required to make this work.

And the usage code:

var p = Person(id: 123, fname: "David")
p.id
p.fname

let id: Int = p.getValue("id")
let fname: String = p.getValue("fname")

p.setValue(21, forKey: "id")
p.setValue("Sally", forKey: "fname")

let id1: Int = p.getValue("id")
let fname1: String = p.getValue("fname")

The full playground source can be found here: https://gist.github.com/owensd/82af8e362273e46d70f9.

I'll leave it up to you on how useful this is.

Key Value Coding in Swift