KeyPath in Swift: Usage.
Sergey Leschev
Posted on January 29, 2023
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
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
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")
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)
}
}
}
}
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)
}
}
}
}
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]
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"]
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"]
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
Posted on January 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.