Keypaths in Swift Explained

Written by Reinder de Vries on October 5 2020 in App Development, Swift

Keypaths in Swift Explained

With keypaths in Swift, you’re getting a reference to a property directly, as opposed to its value. You can pass these keypaths around in your code, and create all sorts of metaprogramming mayhem with it. Let’s find out more!

In this tutorial, we’ll discover:

  • What keypaths are and why they’re useful in Swift
  • How to construct a keypath, including its type
  • How you can create your own functions that use keypaths
  • Tricks and caveats with WritableKeyPath, Any and generics
  • And of course, plenty example code

Ready? Let’s go.

  1. What’s a KeyPath in Swift?
  2. Why Use Keypaths?
  3. Passing Keypaths Around in Your Code
  4. Fun with Keypaths in Swift
  5. Further Reading

What’s a KeyPath in Swift?

Keypaths in Swift are a way of storing a reference to a property, as opposed to referencing property’s value itself. It’s like working with the name of the property, and not its value.

Let’s take a look at an example:

struct Videogame {
    var title:String
    var published:String
    var rating:Double
}

let cyberpunk = Videogame(title: "Cyberpunk 2077", published: "2020", rating: 5)
let titleKeyPath = \Videogame.title

print(cyberpunk[keyPath: titleKeyPath]) // Output: Cyberpunk 2077

In the above code, we’ve defined a struct Videogame with a few properties. We’re also creating a Videogame object and assign it to cyberpunk, using the memberwise initializer.

Next, we’re creating a keypath and assign it to the titleKeyPath constant. The key path itself is \Videogame.title, which is a reference to the title property of the Videogame struct.

On the last line, we’re using the titleKeyPath to read the value of title on the cyberpunk object. That’s what a keypath is!

Get hired as an iOS developer

Learn to build iOS 14 apps with Swift 5

Sign up for my iOS development course, and learn how to start your career as a professional iOS developer.

Why Use Keypaths?

In the previous example, we’ve used the keypath \Videogame.title to read the property title of a Videogame object. Why would you use a keypath to read it, why not just use cyberpunk.title directly? Let’s find out.

Keypaths are a form of metaprogramming. You dynamically read (or write) an object’s properties, using a reference to the property. The property name title becomes a value. Metaprogramming, in this sense, is using the code itself as data. We can take the keypath \Videogame.title and pass it around in our code.

Let’s take a step back, and make a comparison with dictionaries. Check this out:

let cyberpunk:[String: Any] = [
    "title": "Cyberpunk 2077",
    "published": "2020",
    "rating": 5
]

let path = "title"
print(cyberpunk[path]!)

In the above code, we’ve created a similar data structure as the Videogame struct. The "title" key is assigned to path, and we use it to read the title value from the cyberpunk dictionary.

So far so good, but …

  • … what if the "title" key doesn’t exist in the dictionary?
  • … what if the returned value is of a different type than you expect?
  • … what if you make a typo and type "ttile"?

At best, you’ll find out when your app runs and crashes. At worst, you’ll introduce a bug into your code that takes some time to debug. With keypaths, you won’t make the same mistake, because they’re typed and checked at compile-time.

In the previous section, we’ve worked with the following keypath:

let titleKeyPath = \Videogame.title
print(cyberpunk[keyPath: titleKeyPath])

The type of titleKeyPath is WritableKeyPath<Videogame, String>. We can infer a few things from the type:

  • It’s a generic called WritableKeyPath, because the title property we referenced is declared with var, so it’s writable (or “mutable”). Would we have declared it with let (immutable), the type would have been KeyPath<···>.
  • You can see the name of the struct Videogame in the type, as well as String. These are clearly references to the Videogame struct itself, as well as a reference to the type String of the title property.

Because of these concrete types, Swift can check at compile time that the keypath you’re using in cyberpunk[keyPath: titleKeyPath] is valid. Swift can check whether it exist, whether the type matches, and if the keypath is readable or writable. This makes your code less error-prone, safer, and more productive – that’s exactly what the Swift programming language excels at!

Keypaths in Swift have a few more types, which mostly revolve around type-erasure, like with Any. When you combine or allow multiple keypaths, for example in an array, you can use the PartialKeyPath and AnyKeyPath to construct types that fit multiple keypaths.

Passing Keypaths Around in Your Code

The real reason keypaths are so awesome, is because you can use them like values. You can pass references to properties around in your code, and do all sorts of things with them.

Check out the following code:

extension Array
{
    func sorted<Value: Comparable>(
        keyPath: KeyPath<Element, Value>,
        by areInIncreasingOrder:(Value, Value) -> Bool) -> [Element] {

        return sorted { areInIncreasingOrder(
            $0[keyPath: keyPath], $1[keyPath: keyPath]) }
    }
}

In the above code, we create an extension for the Array type, adding in a new sorted(keyPath:by:) function. This function works the same way as sorted(by:), so it’ll take a closure that determines the sorting. In addition to that, sorted(keyPath:by:) takes a keypath that should be used for sorting.

Here’s an example:

let games = [
    Videogame(title: "Cyberpunk 2077", published: "2020", rating: 999),
    Videogame(title: "Fallout 4", published: "2015", rating: 4.5),
    Videogame(title: "The Outer Worlds", published: "2019", rating: 4.4),
    Videogame(title: "RAGE", published: "2011", rating: 4.5),
    Videogame(title: "Far Cry New Dawn", published: "2019", rating: 4),
]

for game in games.sorted(keyPath: \Videogame.rating, by: >) {
    print(game.title)
}
// Output: Cyberpunk 2077, Fallout 4, RAGE, The Outer Worlds, Far Cry New Dawn

Here’s what happens in the above code:

  1. A bunch of Videogame objects are added to the games array
  2. With games.sorted(···), the games are sorted by the keypath \Videogame.rating in descending order
  3. Finally, the games’ titles are printed out

Now that you’ve coded this function, you can use any kind of keypath to sort the games array. You could sort by title, published date, rating, and so on. Instead of hard-coding those properties, you pass the keypath into the sorted(···) function, to dynamically sort by property. That’s the power of metaprogramming with keypaths!

Credits for the sorted(keyPath:by:) code go to Cal Stephens, who proposed to add keypath-based sorting to the Swift language itself. The code works by defining a generic function for arrays, which accepts a keypath in the form of KeyPath<Element, Value>, where Element is the object we’re sorting and Value is the type of the sort property. Inside the function, the standard sorted(by:) function is used to sort the array based on a predicate. This predicate is the areInIncreasingOrder closure, which will return true if $1[keyPath: keyPath] should be ordered after $0[keyPath: keyPath]. When you replace those values with the actual values from the array, based on the predicate, you see how it’s similar to something like sorted(by: { $0.rating > $1.rating }).

Fun with Keypaths in Swift

Now that we’re here, let’s have some more fun with keypaths. Check out the following function:

extension Array {
    func column<Value>(_ keyPath: KeyPath<Element, Value>) -> [Value] {
        return map { $0[keyPath: keyPath] }
    }
}

games.column(\Videogame.title)
// ["Cyberpunk 2077", "Fallout 4", "The Outer Worlds", "RAGE", ···]

What’s going on here? The column(_:) function will return values for a specific property of objects in an array, kinda like one column of a spreadsheet. In the above code, we’re getting the values for the keypath (i.e., property) \Videogame.title from the games array.

The map(_:) function is implicitly called on self, i.e. on the current array. It essentially maps the array of objects, to an array of specific values, based on the keypath. In the above code, $0[keyPath: keyPath] is equal to individual strings for title.

In the above code, Value and Element are generic placeholders. They’re not actual Swift types, but they’re merely placeholders that get switched out when you compile the code. The generic function column(_:) requires that the type of the array that’s returned, i.e. [Value], is the same as the type of the property in the keypath. So when the property title has type String, the returned array has type [String]. When you use \Videogame.rating as the keypath, the returned array must have type… [Double]!

Alright, let’s write some more code. What about reversing a specific property on an array of objects? We can do that with keypaths. Check this out:

extension Videogame {
    mutating func reverse(_ keyPath: WritableKeyPath<Self, String>) {
        self[keyPath: keyPath] = String(self[keyPath: keyPath].reversed())
    }
}

With the above function, we can reverse any property of the Videogame struct that’s a string and writable. The type WritableKeyPath<Self, String> refers to any property that’s declared with var, of type Self, which is a reference to Videogame, and property type String.

Here’s how it works:

var game = Videogame(title: "Cyberpunk 2077", published: "2020", rating: 999)
game.reverse(\Videogame.title) // 7702 knuprebyC

And with little effort, we can reverse any other String property too:

game.reverse(\Videogame.published) // 0202

OK, OK, one more… Check out this extension for Array:

extension Array 
{
    func find<Value: Comparable>(where keyPath: KeyPath<Element, Value>, equals value: Value) -> Element?
    {
        for item in self {
            if item[keyPath: keyPath] == value {
                return item
            }
        }

        return nil
    }
}

Here’s how you’d use it:

let games = [···]

if let game = games.find(where: \Videogame.published, equals: "2011") {
    print(game.title) // Output: RAGE
}

With the find(where:equals:) we can search an array of objects, and find an item for which a property is equal to a given value. The find(···) function relies on a keypath to select the property that we want to match with a value. The difference with using a function like firstIndex(where:), is that the above function works with any property/keypath. Neat!

These examples are a bit contrived of course – unless you actually want to reverse string properties at scale – but they do show how a little programming around types and properties goes a long way towards making flexible, composable code. Awesome!

Get hired as an iOS developer

Learn to build iOS 14 apps with Swift 5

Sign up for my iOS development course, and learn how to start your career as a professional iOS developer.

Further Reading

Keypaths are one of those Swift tools that you didn’t know you needed. They help you write more flexible Swift code, and once you’ve abstracted some of it away, they’re quite fun to work with!

Here’s what we’ve discussed in this tutorial:

  • What keypaths are and why they’re useful in Swift
  • How to construct a keypath, including its type
  • How you can create your own functions that use keypaths

Want to learn more? Check out these resources:

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Start your iOS career
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy