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.