Swift KeyPaths

What the differences are and how to use them sorting a collection.

Joshua Brunhuber
joshtastic-blog
4 min readJun 26, 2019

--

Photo by Matt Artz on Unsplash

I sometimes stumble upon KeyPaths in everyday life but was never aware of what‘s going on under the hood. I took a closer look and want to share my insights with you.

What is a „KeyPath“?

KeyPaths are references to a property of a type. They‘re useful when you want to perform operations depending on that property, rather on the value behind it. This makes them useful especially for handling collections and key-value observing.

KeyPaths before Swift 4

KeyPaths already exist in Objective-C and you can use them in Swift too.

#keyPath(Person.firstName)

Technically, the „old“ KeyPaths are just strings, which are slow to parse and aren‘t able to story type information. That‘s why they aren‘t considered as type-safe. You also have to deal with memory management when using them with key-value observation together with UIKit. The observed instance might still exist, even the observer is already deallocated. This leads to zombies in your App and if you watched The Walking Dead, you now know which chaos that means 🧟‍♂️🧟‍♀️

Swift KeyPaths

With the release of Swift 4, new KeyPaths were introduced. Instead of using strings as references, they’re implemented using generic classes. KeyPaths are syntactically specified with a backslash \

let nameKeyPath = \Cat.name

The \ is required to avoid problems with static properties.

struct Cat {
static var intergalagticCatForce: String = "Purring"
var name: String
}

Note: KeyPaths don’t work with static properties.

You can also use KeyPaths though nested-types:

struct Food {
var calories: Float
}
struct Cat {
var favoriteFood: Food
}
let calorieKeyPath = \Cat.favoriteFood.calories

Or append them:

let foodKeyPath = \Cat.favoriteFood
let calorieKeyPath = foodKeyPath.appending(path: \Food.calories)

Reading and writing values using KeyPaths

You can use KeyPaths to get and set values on instances. This is a good way to understand the different types of KeyPaths. Let‘s get the cat‘s name using the nameKeyPath:

struct Cat {
let name: String
var color: String
}

let enchilada = Cat(name: "Enchilada", color: "Brown")
let name = enchilada[keyPath: \Cat.name]

If you print the type of nameKeyPath, you‘ll notice that it‘s a KeyPath<Cat, String>.

// KeyPath
print(type(of: \Cat.name))

It’s not surprising but let‘s check out the type when we create a KeyPath on the cats color.

Here we have WritableKeyPath<Cat, String>, a subclass of KeyPath. The KeyPath is writable, because the property is defined using var instead of let, thus it allows us to change the value. Let‘s change the color of the cat:

var enchilada = Cat(name: "Enchilada", color: "Brown")
enchilada[keyPath: \Cat.color] = "Pink"

Great, we successfully changed the color of the cat. But there‘re two types of writable KeyPaths. To demonstrate this, I‘ll declare a new type „Dog“, but this time, I‘m using a class instead of a struct.

class Dog {
var color = "blue"
}
let colorKeyPath = \Dog.color

If you try out the example from above, you‘ll notice that now it‘s not a WritableKeyPath; it‘s a ReferenceWritableKeyPath<Dog, String>.

The difference between Writable- and ReferenceWritableKeyPaths

If you think for WritableKeyPath<Root, Value> as a function for the set-action: (Root, inout Value) -> Void, you‘ll need to declare Value as inout parameter, so the value can be mutated. This isn‘t necessary for reference types therefore ReferenceWriteableKeyPath<Root, Value> setter function is like (Root, Value) -> Void. Pretty much the same, but without the inout Parameter.

Partial- and AnyKeyPath

When you try to store multiple KeyPaths in a collection, you may run into type-problems. Let‘s take the Cat example and save both color and name KeyPaths.

let nameKeyPath = \Cat.name
let colorKeyPath = \Cat.color
let keyPaths = [nameKeyPath, colorKeyPath]
print(type(of: keyPaths))

The array has the type Array<WritableKeyPath<Cat, String>> because the properties are mutable and have the same type.
Let‘s declare an age property as an integer and try it again:

let keyPaths = [nameKeyPath, colorKeyPath, ageKeyPath]
print(type(of: keyPaths))

Now, the generic type of the array is PartialKeyPath<Cat>. PartialKeyPaths store the type-information from the Root type, but not from the Value type. By using this type of KeyPath, we know that our Root element is Cat, but we don‘t know what our Valueis.

We can take this even further: Let‘s think about to store KeyPaths from our Dog class. In this case, even the Root type doesn’t match. This fully erases the KeyPath to AnyKeyPath<Any, Any>.

Sorting a collection

One good use-case of KeyPaths is sorting a collection. There has already been a pitch on the Swift forum to implement the feature in the standard library Sort Collection using KeyPath on Swift Forums. But unfortunately, this hasn’t landed into Swift yet so let’s write a sort function by yourself.

This function takes a keyPath and another areInIncreasingOrder function. areInIncreasingOrder specifies whether the order should be descending or ascending. If you take a closer look, you’ll notice that the function itself just returns the default sort-function using areInIncreasingOrder with the keyPath subscript access on the properties.

Sorting collections like this never was so easy:

let tacco = Cat(name: "Tacco", favoriteFood: Food(name: "Tacco 🌮", calories: 723))
let nala = Cat(name: "Nala", favoriteFood: Food(name: "Fish 🐟", calories: 340))
let cats = [whiskers, tacco, nala]
let sortedByName = cats.sorted(on: \Cat.name, by: <)
let sortedByFavouriteFoodKcal = cats.sorted(on: \Cat.favoriteFood.calories, by: <)

The syntax makes it immediately clear how the collection gets sorted without passing complex closures around.

Conclusion

KeyPaths are nothing I use every day but the syntax allows us to write more understandable code. Something I wish would be better integration for Key-Value observation on UIKit classes. It would be nice to observe UITextField on .\name and update UI e. g. search results. At the moment this is only possible using 3rd party technologies like ReactiveX.

The playground is available on GitHub: https://github.com/jbrunhuber/joshtastic-simples/tree/master/KeyPaths

--

--

Joshua Brunhuber
joshtastic-blog

iOS Developer📱 Nature🌲🍂 I also like music 🎸 and photography 📷