Swift keypaths


Greetings, traveler!

Swift keypaths have become a pretty popular feature since SwiftUI uses them a lot. So, let’s talk about them a little. We have already discussed Swift keypaths in this article. Let’s refresh our memory. Swift keypaths allow us to store a path to an object’s property. Keypaths have cool features. Let’s consider some of them.

KeyPath, WritableKeyPath and ReferenceWritableKeyPath

First of all, there are three different types of keypaths.

Here is an example of a KeyPath.

struct User {
    let name: String = ""
}

let keyPath = \User.name // KeyPath<User, String>

As you see, the User’s name is a let constant. But what if we will make it mutable? Our keyPath will become a WritableKeyPath then.

struct User {
    var name: String = ""
}

let keyPath = \User.name // WritableKeyPath<User, String>

And if this object was a class, the keypath’s type was a  ReferenceWritableKeyPath.

class User {
    var name: String = ""
}

let keyPath = \User.name // ReferenceWritableKeyPath<User, String>

Let’s make this property immutable. The keypath will have a KeyPath type again.

class User {
    let name: String = ""
}

let keyPath = \User.name // KeyPath<User, String>

Subscript keypath

We can use Swift keypaths not only as a reference to an object property but also as a subscript keypath. Let’s consider an example.

struct User {
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

let users = [
    User(name: "Jack"),
    User(name: "Bella")
]

We can get the name value of the first element in this array with the keypath.

let subscriptKeyPath = \[User].[0].name
users[keyPath: subscriptKeyPath] // "Jack"

# Dynamic member lookup

Consider this code.

enum EngineKind {
    case petrol
    case diesel
    case hybrid
}

struct Engine {
    let kind: EngineKind
    let horsePower: Int
}

struct Vehicle {
    let name: String
    let engine: Engine
}

To get the horsepower value, we need to write this code.

let vehicle = Vehicle(name: "Green car", engine: .init(kind: .diesel, horsePower: 200))

vehicle.engine.horsePower

However, we can access the desired value more conveniently with the dynamic member lookup and a KeyPath.

@dynamicMemberLookup
struct Vehicle {
    let name: String
    let engine: Engine
    
    subscript<T>(dynamicMember keyPath: KeyPath<Engine, T>) -> T {
        engine[keyPath: keyPath]
    }
}

let vehicle = Vehicle(name: "Green car", engine: .init(kind: .diesel, horsePower: 200))

vehicle.horsePower

You can read more about the @dynamicMemberLookup attribute here.

Using as functions

Since keypaths are conceptually similar to functions, we can treat them like functions and pass them directly into the closures.

let autoPark = [
    Vehicle(name: "Blue car", engine: .init(kind: .hybrid, horsePower: 150)),
    Vehicle(name: "Black car", engine: .init(kind: .diesel, horsePower: 200)),
    Vehicle(name: "Red car", engine: .init(kind: .petrol, horsePower: 170))
]

autoPark.map(\.name)

Value comparison

You can use a keypath for comparison, but some initial preparation is needed. Let’s take a look at this example.

let autoPark = [
    Vehicle(name: "Blue car", engine: .init(kind: .hybrid, horsePower: 150)),
    Vehicle(name: "Black car", engine: .init(kind: .diesel, horsePower: 200)),
    Vehicle(name: "Red car", engine: .init(kind: .petrol, horsePower: 170))
]

autoPark.filter(\.engine.horsePower > 150) // ❌

It won’t compile, but we can fix it easily. To do it, we need to create the ‘>’ method. We will use the lhs as a KeyPath and the rhs as a constant conforming to a Comparable protocol. After that, we can use the KeyPath to compare the property’s value to that constant.

func ><Root, Value: Comparable>(
    _ lhs: KeyPath<Root, Value>,
    _ rhs: Value
) -> (Root) -> Bool {
    
    { $0[keyPath: lhs] > rhs }
}

autoPark.filter(\.engine.horsePower > 150) // ✅

Conclusion

Some modern and popular frameworks, such as SwiftUI and Combine, have revived interest in Swift keypaths. Therefore, it is important to know how to work with this tool today.