KVC in Swift (Part 2)

3 Jul

Continuing my attempt at making KVC in Swift

First some quick follow-up:

Paul Lynch mentions on twitter that KVC has actually been around since NeXTSTEP 3.3 (or maybe earlier).

David Owens shows us how simple it is to convert the valueForKey: method to generics.


 

This time around we’ll integrate David’s generic functionality, see how subscripting with generics still isn’t quite there, and extend our KVC protocol to implement valueForKeyPath:

First up, switching to generics – where we can.

In our KVC protocol change valueForKey: to the following

func valueForKey(key : String) -> T?

Then in the KVC extension change the valueForKey: default implementation to this:

func valueForKey<T>(key : String) -> T? {

    let mirror = reflect(self)
    
    for index in 0 ..< mirror.count {
        let (childKey, childMirror) = mirror[index]
        if childKey == key {
            return childMirror.value as? T
        }
    }
    return nil
}

You probably noticed the only real difference is replacing Any with T. T is just a placeholder for a type, a generic type if you will. We could have just as well named it MySuperAwesomeGenericType, but we’re not crazy.

Generic functions can work with any type. The actual type that will be used in place of T will be determined each time valueForKey is called. Confusing? Maybe this will help…

Before, if we had called valueForKey: it would return a type of Any.

let s = aValue.valueForKey("someString")

We can’t do anything useful without casting the type.

let s = aValue.valueForKey("someString") as? String

With generics we can write this more naturally and it will return the type we specify.

let s : String? = aValue.valueForKey("someString")

It would be really nice if the compiler could infer the type entirely, but it’s a start.

Unfortunately this doesn’t translate directly to subscripting.

You can’t just switch out the Any? with T? here

subscript (key : String) -> T?

The compiler will complain that it doesn’t know what T is. I think there might be a way around this by using typealias. If you know of a way to make the subscript generic I would love to hear about it (@leemorgan).

OK. Now that we’ve cleaned that up a bit. Let’s take a look at our next method to implement, valueForKeyPath:

Unlike valueForPath: which will only let you get the value of a given object, valueForKeyPath: will evaluate a chain of properties and return the final link’s value.

We’ll get started by adding the prototype to our KVC protocol

func valueForKeyPath<T>(keyPath : String) -> T?

Then in our KVC extension we’ll implement it like so…

func valueForKeyPath<T>(keyPath : String) -> T? {
    
    let keys = split(keyPath.characters){$0 == "."}.map{String($0)}
    var mirror = reflect(self)
    
    for key in keys {
        for index in 0 ..< mirror.count {
            let (childKey, childMirror) = mirror[index]
            if childKey == key {
                if childKey == keys.last {
                    return childMirror.value as? T
                }
                else {
                    mirror = childMirror
                }
            }
        }
    }
    return nil
}

Most of this should look very familiar, but there’s two parts that need explaining…

let keys = split(keyPath.characters){$0 == "."}.map{String($0)}

There’s a lot going on in that line, but once we break it down it’s not that bad.

In Swift 2 you can’t use split() on String anymore. To get around this we use the characters property of String and split whenever we encounter a “.”

Next, since we’re splitting on the characters we need to convert them back into Strings. We use map to return the split character groups as Strings. (Credit to AirSpeedVelocity for detailing this change in Swift 2, and much more here).

The other part we need to explain is the new loop.

for key in keys {
...
if childKey == keys.last { 
    return childMirror.value as? T 
} 
else { 
    mirror = childMirror 
}

This loop steps through each key in the property key chain. If the key is the last key in the chain we return the value like we did before in valueForKey: otherwise we set mirror to point to the child property and continue the loop.

Now we can quickly transverse a chain of properties

struct myValue : KVC {
    let someString = "Hello World"
}

struct myValueHolder : KVC {
    let aValue = myValue()
}
let bValue = myValueHolder()

let c : String? = bValue.valueForKeyPath("aValue.someString")

And if we change our subscript to use valueForKeyPath: instead of valueForKey: we get this ability for free in subscripting as well.

let c : Any? = bValue["aValue.someString"]

And here’s a cool feature; only the root object needs to conform to the KVC protocol. Even if it’s properties, their children, and so on, don’t conform to KVC you can still access them using valueForKeyPath:

This is still more of a proof of concept than anything, but I’m having fun seeing how KVC could be very natural in Swift.

Updated playground on GitHub here.