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.