Swift and valueForKeyPath: You Can’t Keep A Good API Down!
tl;dr; Download the Value For Key Path Playground
I was incredibly disappointed in Swift when I started to look into how key value coding might be preserved in the transition from Objective-C. Of course, Apple would preserve that, right? And I don’t mean by resorting to using Foundation classes–you know all type-casty and everything (yeah, type-casty). Are we progressing forward or looking to the past, after all? I shouldn’t have to rely on NSArray like this:
var values = (items as NSArray).valueForKeyPath("value")
Right? Who wants to do that? What’s the proper Swift way?
Well, my early assumption from what I was seeing in Swift was that valueForKeyPath: was gone and no longer did we have a convenient way to grab only the fields we needed from a collection of objects. It appeared that it couldn’t be done–at least not in any idiomatic elegant way. Then I downloaded some sample code from the Apple developer site and noticed they were doing what I needed, but using map to do it. The answer was map. Here’s an example:
var lastNames = items.map({$0["last"]! as String})
Now the lastNames variable contains a list of strings with just the last name property of my items array. Given an array of dictionaries like this, you can see how that is providing pretty much what we used to get from valueForKeyPath:
var items = [
["first" : "Billy", "last" : "Bogart", "address" : ["street" : "111 Main Street"]],
["first" : "Gary", "last" : "Gollum", "address" : ["street" : "2277 AB Street"]],
["first" : "David", "last" : "Dangerly", "address" : ["street" : "22311 Place Ave."]],
["first" : "Johnny", "last" : "Jones", "address" : ["street" : "10 Orange Rd."]]
]
lastNames now contains:
["Bogart", "Gollum", "Dangerly", "Jones"]
Notice we also have an address property that points to a dictionary. If want to get the street property inside of address, we can drill down the same way:
var streetAddresses = items.map({ $0["address"]!["street"] as String})
Now, you can see we had to force-unwrap the address property in order to access the street property. This works perfectly and is a fine replacement for valueForKeyPath: of old. When the call succeeds we now have all of the street addresses in the streetAddresses array:
["111 Main Street", "2277 AB Street", "22311 Place Ave.", "10 Orange Rd."]
Take a look at the Value For Key Path Playground if you want to see it in action.
What About Safety?
In Objective-C, if you message nil, it’s a no-op. In Swift, however, if we unwrap an optional that is nil, we will crash. So, on this line:
var streetAddresses = items.map({ $0["address"]!["street"] as String})
when we unwrap using the bang ! operator, if that value doesn’t exist, we’re in trouble. Say our data was changed to this:
var items = [
["first" : "Billy", "last" : "Bogart", "addr" : ["street" : "111 Main Street"]],
["first" : "Gary", "last" : "Gollum", "addr" : ["street" : "2277 AB Street"]],
["first" : "David", "last" : "Dangerly", "addr" : ["street" : "22311 Place Ave."]],
["first" : "Johnny", "last" : "Jones", "addr" : ["street" : "10 Orange Rd."]]
]
Notice the address field has been changed to addr. This causes a crash.
Meanwhile, with the old Foundation way, we do this:
var oldStreetAddresses = (items as NSArray).valueForKeyPath("address.street") as NSArray
but look what the output is. NSNull!
[NSNull, NSNull, NSNull, NSNull]
No crash. So which is better?
Well, if your data can be unpredictable, which really shouldn’t happen, then the Foundation outcome is “safer” from a crash perspective. However, your data should be nailed down, so a crash would actually tell you what’s wrong with your data pretty quickly.
What, though, is the safe Swift way of handling this? We can coerce our objects to optionals and use optional chaining instead:
var streetAddresses = items.map({ $0["address"]?["street"] as? String})
So our first optional operator says to return the object or nil and the second operator says that is should be an optional of type String. This call returns an array of optionals that has this output in our playground:
[nil, nil, {Some "22311 Place Ave."}, {Some "10 Orange Rd."}]
Assuming the following data:
var items = [
["first" : "Billy", "last" : "Bogart", "addr" : ["street" : "111 Main Street"]],
["first" : "Gary", "last" : "Gollum", "addr" : ["street" : "2277 AB Street"]],
["first" : "David", "last" : "Dangerly", "address" : ["street" : "22311 Place Ave."]],
["first" : "Johnny", "last" : "Jones", "address" : ["street" : "10 Orange Rd."]]
]
Notice the first two use the bad addr field and the last two use the correct address field.
For my applications, I think I would prefer to see a crash so I know my data is bad. It’s the server developer’s problem. Am I right? ;-)
Taking Map() Farther
The map() function really goes far beyond this trivial little valueForKeyPath: replacement example. You can really massage your data into any form you want with it. Say for example you wanted to create an transformed array of dictionaries from your array of dictionaries that had some of the same data, but organized differently. For example:
var fullNames = items.map({ ["full_name" : ($0["first"] as? String)! + " " + ($0["last"] as? String)!] })
This returns an array of dictionaries that contain a single key value pair with a key called full_name and value of the first name and last name of each item in the original array concatenated together. The output looks like this in our playground:
[["full_name": "Billy Bogart"], ["full_name": "Gary Gollum"], ["full_name": "David Dangerly"], ["full_name": "Johnny Jones"]]
As you can see, there really are a lot of possibilities for massaging your data structures using map. The tough part is imagining how it might be used and often that just comes from experience. The more you use it, the more obvious the solutions will become.
Swift Is Not So Bad
There are certain complaints about Swift I still have, however, I am finding that it is growing on me. I am starting to have much more success making my code pure Swift–pulling back from my old habits and considering how problems should be solved in this brave new Swift world. It’s getting better and some of the language aspects are actually better than Objective-C. I couldn’t see that forest for the trees just a couple short months ago. Until next time.