DEV Community

Cover image for KeyPath in Swift: Usage.
Sergey Leschev
Sergey Leschev

Posted on • Updated on

KeyPath in Swift: Usage.

A key path in Swift refers to a property or subscript of a type, so its only usage is to read/write to that property/subscript using the key path.

Access a value

To access a value using a key path, pass a key path to the subscript(keyPath:) subscript, which is available on all types. You can use it to read or write based on the type of a key path and an instance.

Her is an example of using key path to read/write to user.role.

var user = User(
    name: "Sergey",
    email: "sergey.leschev@gmail.com",
    address: nil,
    role: .admin)

let userRoleKeyPath = \User.role
//  WritableKeyPath<User, Role>

// 1
let role = user[keyPath: userRoleKeyPath]

print(role) // admin

// 2
user[keyPath: userRoleKeyPath] = .guest

print(user.role) // guest
Enter fullscreen mode Exit fullscreen mode

1 Use keypath to read the role value.
2 Use keypath to set the role value.

One thing to note here is that even with WritableKeyPath, your struct still needs to be var to be able to write. Try to set a new value on a let value would cause a compile error.

WritableKeyPath can use on both let and var for reference types.

Caveats

Constructing a key path using unsafe expressions can cause the same runtime error as using them on an instance.

Here is an example using forced unwrapping expressions (!) and array subscript(index: Int) in key paths.

let fourthIndexInteger = \[Int][3]
let integers = [0, 1, 2]
print(integers[keyPath: fourthIndexInteger])
// Fatal error: Index out of range

let user = User(
    name: "Sergey",
    email: "sergey.leschev@gmail.com",
    address: nil,
    role: .admin)

let forceStreetAddress = \User.address!.street
print(user[keyPath: forceStreetAddress])
// Fatal error: Unexpectedly found nil while unwrapping an Optional value
Enter fullscreen mode Exit fullscreen mode

Identity Key Path

We also have a special path that can refer to a whole instance instead of a property. We can create one with the following syntax, \.self.

The result of the identity key path is the WritableKeyPath of the whole instance, so you can use it to access and change all of the data stored in a variable in a single step.

var foo = "Foo"
// 1
let stringIdentity = \String.self

//  WritableKeyPath<String, String>

foo[keyPath: stringIdentity] = "Bar"
print(foo) // Bar

struct User {
  let name: String
}
var user = User(name: "John")
// 2
let userIdentity = \User.self

// WritableKeyPath<User, User>

user[keyPath: userIdentity] = User(name: "Doe")
print(user) // User(name: "Doe")
Enter fullscreen mode Exit fullscreen mode

1 Identity key path to String.
2 Identity key path to User.

Use Cases

Key paths seem like another way of reading and writing value out of an instance. But the fact that we can treat an ability to read/write a value in the form of a variable makes the use cases broader than read and write.

It is okay if you can't think of any use cases of key paths. As I mentioned initially, it is a kind of metaprogramming that is needed for some specific scenario.

It is quite hard to tell you exactly where you should use the key paths. I think it is easier to show you where they are used. If you have seen enough use cases, I think you will eventually know where you can use them (or don't).

Here are some places where key paths are used in real API.

Key paths as protocols alternative

In SwiftUI, we can create views from a collection of Identifiable data. The only requirement of the Identifiable protocol is a Hashable variable named ID.

struct User: Identifiable {
    let name: String

    // 1
    var id: String {

        return name
    }
}

let users: [User] = [
    User(name: "John"),
    User(name: "Alice"),
    User(name: "Bob"),
]

struct SwiftUIView: View {
    var body: some View {
        ScrollView {
            ForEach(users) { user in
                Text(user.name)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

1 Use name to uniquely identify user. This is for demonstration only, you should use something more unique for an ID, or bad things will happen with your list.

Identifiable is a protocol to uniquely identify an item in a list. SwiftUI also provides an alternative initializer using a key path.

KeyPath

Instead of forcing data type to conform Identifiable protocol, this alternative initializer let data type specified a path to its underlying data identity.

// 1
struct User {

    let name: String
}

struct SwiftUIView: View {
    var body: some View {
        ScrollView {
            // 2

            ForEach(users, id: \.name) { user in
                Text(user.name)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

1 User no longer conform to Identifiable protocol.
2 We specify path to property that can uniquly identify User struct.

Instead of using a protocol to define a common interface for getting some value, we can use a key path to inject that value instead. Keypath provided a way to transfer read access to other functions.

The interesting point here is the ability to reference to read/write access resulting in the equivalent functionality as Identifiable protocol. The scope of key paths can be broader than just read/write.

Key paths as functions

We can also look at a key path in the form of function.

The key path expression \Root.value can represent as a function with the following signature (Root) -> Value.

In this example, we try to map user names out of an array of users.

map(_:) has the following signature. It accepts a transform parameter closure with array element (Element) as argument and return type that you want to transform to (T).

func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
Enter fullscreen mode Exit fullscreen mode

Let try it without a key path.

struct User {
    let name: String
}

let users = [
    User(name: "Sergey"),
    User(name: "Alice"),
    User(name: "Bob")
]

let userNames = users.map { user in
    return user.name
}
// ["Sergey", "Alice", "Bob"]
Enter fullscreen mode Exit fullscreen mode

In this example, map(_:) accept a parameter of function (Element) -> Value. Based on our claim, we should be able to use a key path expression \Element.Value instead. Let's try to create a new override of a map that takes a key path instead.

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

let userNames = users.map(\.name)
// ["Sergey", "Alice", "Bob"]
Enter fullscreen mode Exit fullscreen mode

As you can see, we can create an equivalent implementation for a function that expected (Root) -> Value with a key path of \Root.Value. In Swift 5.2, we don't even have to do the conversion ourselves. This functionality is built right into the Swift under this proposal.

As a result, a key path expression \Root.value can use wherever functions of (Root) -> Value are allowed.


Previous Articles:


Contacts
I have a clear focus on time-to-market and don't prioritize technical debt. And I took part in the Pre-Sale/RFX activity as a System Architect, assessment efforts for Mobile (iOS-Swift, Android-Kotlin), Frontend (React-TypeScript) and Backend (NodeJS-.NET-PHP-Kafka-SQL-NoSQL). And I also formed the work of Pre-Sale as a CTO from Opportunity to Proposal via knowledge transfer to Successful Delivery.

πŸ›©οΈ #startups #management #cto #swift #typescript #database
πŸ“§ Email: sergey.leschev@gmail.com
πŸ‘‹ LinkedIn: https://linkedin.com/in/sergeyleschev/
πŸ‘‹ LeetCode: https://leetcode.com/sergeyleschev/
πŸ‘‹ Twitter: https://twitter.com/sergeyleschev
πŸ‘‹ Github: https://github.com/sergeyleschev
🌎 Website: https://sergeyleschev.github.io
🌎 Reddit: https://reddit.com/user/sergeyleschev
🌎 Quora: https://quora.com/sergey-leschev
🌎 Medium: https://medium.com/@sergeyleschev
πŸ–¨οΈ PDF Design Patterns: Download

Top comments (0)