26
loading...
This website collects cookies to deliver better user experience
class User {
var fullName: String
var email: String?
var age: Int
init(fullName: String, email: String? = nil, age: Int) {
self.fullName = fullName
self.email = email
self.age = age
}
}
let user = User(fullName: "Mike", age: 16)
let fullNameKeyPath = \User.fullName
// get fullName value using fullNameKeyPath
print( user[keyPath: fullNameKeyPath] ) // Mike
// set fullName value using fullNameKeyPath
user[keyPath: fullNameKeyPath] = "John"
print( user[keyPath: fullNameKeyPath] ) // John
\Root.path
and the KeyPath declaration isclass KeyPath<Root, Value> : PartialKeyPath<Root>
Root Type
( User is the example ) and Value Type
which is the Type of the property the keyPath refers to ( String in case fullName, Int for age, String? for email ).Note: at compile time, a key-path expression ( \Root.path
) is replaced by an instance of the keyPath class of the appropriate keyPath type ( we have 5 types of keypaths )
class User {
// ...
}
let user = User(fullName: "Mike", age: 16)
let fullNameEmptyKeyPath = \User.fullName.isEmpty
print(user[keyPath: fullNameEmptyKeyPath]) // false
let user = User(fullName: "Mike", age: 16)
let fullNamePath = \User.fullName
let isEmptyPath = \String.isEmpty
let fullNameIsEmptyKeyPath = fullNamePath.appending(path: isEmptyPath) // merge the two paths
print( user[keyPath: fullNameIsEmptyKeyPath] ) // read keyPath value -> false
Root
type is User
and the Value
is Bool
( isEmpty is Bool ) so the fullNameIsEmptyKeyPath is of type KeyPath<User, Bool>
ReferenceWritableKeyPath
inherits from WritableKeyPath
and WritableKeyPath
inherits from KeyPath
and so on.Note: that hierarchy means ( for example ) if you have a function that takes KeyPath<Root,Type>
as a parameter then you can pass also a keyPath of type WritableKeyPath
and ReferenceWritableKeyPath
as a parameter to hat function because both of theme inherits from KeyPath<Root,Type>
but you cannot pass in a parameter of type PartialKeyPath
or AnyKeyPath
ReferenceWritableKeyPath
works with Root of reference types ( class ) and mutable properties ( var ), this allows to read and write ( change ) the property value.class User {
var fullName: String
var email: String?
var age: Int
// ...
}
let fullNameKeyPath = \User.fullName
print(type(of: fullNameKeyPath)) // ReferenceWritableKeyPath<User, String>
Note: it doesn't matter wether the instance itself is var or let, what matters is the keyPath property should be mutable ( var ) to be able to change/write its value.
WritableKeyPath
works will Root of value types ( struct and enum ) and again allow us to read/write the keyPath property value of an instance taking into account the property should be var and the instance should be var also.struct User {
var fullName: String
var email: String?
var age: Int
}
var user = User(fullName: "Mike", age: 16) // instance should be var
let ageKeyPath = \User.age
// get fullName
print(user[keyPath: ageKeyPath]) // 16
// set fullName
user[keyPath: ageKeyPath] = 20
print(user[keyPath: ageKeyPath]) // 20
print(type(of: ageKeyPath)) // WritableKeyPath<User, Int>
Root type
. it is used only for read property value ( read-only ) so it is only used for accessing immutable ( let ) properties.struct User { // or it can be -> class User
var fullName: String
var email: String?
let age: Int // Note: age is immutable
}
var user = User(fullName: "Mike", age: 16)
let ageKeyPath = \User.age
// get age
print(user[keyPath: ageKeyPath]) // 16
print(type(of: ageKeyPath)) // KeyPath<User, Int>
ageKeyPath
.user[keyPath: ageKeyPath] = 20 // error: cannot assign through subscript: 'ageKeyPath' is a read-only key path
Root type
( User in our example ) and the Value Type
( String for fullName or Int for age ). But if we don't know the Value type
of our keyPath or we need to be more flexible about the value type then we can use the PartialKeyPath<Root>
which takes a reference to a property of any ValueType
from specific Root type
. this is beneficial when we need to write a functions that accepts a KeyPaths of different Value types but the same Root Type or we need to store multiple keyPaths of different Value types in an array.struct User {
var fullName: String
var email: String?
let age: Int
}
var user = User(fullName: "Mike", email: "[email protected]", age: 16)
let agePartialKeyPath: PartialKeyPath<User> = \User.age
let emailPartialKeyPath: PartialKeyPath<User> = \User.email
let fullNamePartialKeyPath: PartialKeyPath<User> = \User.fullName
let keyPathsList: [PartialKeyPath<User>] = [agePartialKeyPath,emailPartialKeyPath,fullNamePartialKeyPath]
keyPathsList.forEach { keypath in
print(user[keyPath: keypath]) // 16, Optional("[email protected]"), Mike
}
Root Type
is unknown and the Value Type
( Path) is also unknown. this is very beneficial when we want to create an API that takes a keyPath of different Root types or we want to store multiple KeyPaths of different Root Types in an array.struct Cat {
var name: String
}
class User {
// ...
}
var user = User(fullName: "Mike", email: "[email protected]", age: 16)
let userAgeKeyPath = \User.age // ReferenceWritableKeyPath<User, Int>
let userEmailKeyPath = \User.email // ReferenceWritableKeyPath<User, Optional<String>>
let userFullNameKeyPath = \User.fullName // ReferenceWritableKeyPath<User, String>
var cat = Cat(name: "Nala")
let catNameKeyPath = \Cat.name // WritableKeyPath<Cat, String>
let keyPathsList: [AnyKeyPath] = [userAgeKeyPath,userEmailKeyPath,userFullNameKeyPath,catNameKeyPath]
keyPathsList.forEach { keypath in
if let keypath = keypath as? PartialKeyPath<User> {
print(user[keyPath: keypath]) // // 16, Optional("[email protected]"), Mike
}else if let keypath = keypath as? PartialKeyPath<Cat> {
print(cat[keyPath: keypath]) // Nala
}
}
keypath
to PartialKeyPath<User>
( getter return Any ). we can for sure try to cast it to a more specific keypath like KeyPath<Cat, String>
or KeyPath<User, String>
and if the casting succeeds we know exactly both the Root Type
and the Value Type
.KeyPath Type | Read | Write | Note |
---|---|---|---|
AnyKeyPath | Yes | No | unknown Root Type/ unknown Value Type |
PartialKeyPath | Yes | No | known Root Type / unknown Value Type |
KeyPath | Yes | No | known Root Type / known Value Type |
WritableKeyPath | Yes | Yes | known Root Type / known Value Type for value types ( struct and enum ) ( var instance ) |
ReferenceWritableKeyPath | Yes | Yes | known Root Type / known Value Type, for class Type ( let or var instance) |
(Root Type ) -> Value
are common in Swift, Key path as functions is one of the features added to swift 5.2. it allows keypath to automatically be promoted and used wherever a functions of the form (Root ) -> Value
are expected.struct User {
var fullName: String
var email: String?
var age: Int
}
var firstUser = User(fullName: "Mike", email: "[email protected]", age: 20)
var secondUser = User(fullName: "Adam", email: "[email protected]", age: 25)
var ThirdUser = User(fullName: "John", email: "[email protected]", age: 30)
var users = [firstUser,secondUser,ThirdUser]
let usersNames = users.map { $0.fullName } // original
print(usersNames) // ["Mike", "Adam", "John"]
( User ) -> String
return the users full names, in swift 5.2 we can instead pass a keypath for the user fullName property and pass it to the map.let usersNames = users.map (\.fullName) // using keypath
print(usersNames) // ["Mike", "Adam", "John"]
let fullNameKeyPath: (User) -> String = \User.fullName
let usersNames = users.map ( fullNameKeyPath )
print(usersNames)
let fullNameKeyPath: (User) -> String
, because let fullNameKeyPath = \User.fullName
is inferred as a keypath object not as a function ( default behavior ). by making the type explicit we are using the fullNameKeyPath
as a function not as a keypath object.let fullNameKeyPath = \User.fullName // inferred as WritableKeyPath<User, String>
let usersNames = users.map ( fullNameKeyPath ) // Error
print(usersNames)
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
return a[keyPath: keyPath] < b[keyPath: keyPath] // read
}
}
}
var firstUser = User(fullName: "Mike", email: "[email protected]", age: 20)
var secondUser = User(fullName: "Adam", email: "[email protected]", age: 25)
var ThirdUser = User(fullName: "John", email: "[email protected]", age: 30)
var users = [firstUser,secondUser,ThirdUser]
print( users.sorted(by: \.fullName))
// [User(fullName: "Adam", email: Optional("[email protected]"), age: 25),
// User(fullName: "John", email: Optional("[email protected]"), age: 30),
// User(fullName: "Mike", email: Optional("[email protected]"), age: 20)]
print( ["C","D","AB","A"].sorted(by: \.self) ) // ["A", "AB", "C", "D"]
print( ["C","D","AB","A"].sorted(by: \.count) ) // ["C", "D", "A", "AB"]
extension
to the Sequence
type, adding a new sorted(by keyPath:)
that takes a keypath to sort the array based on that keypath value, so we can sort array of users by fullName, email, age or sort any array based on any Comparable property. protocol Builder {}
extension Builder {
func set<T>(_ keyPath:ReferenceWritableKeyPath<Self, T>,to newValue: T) -> Self {
self[keyPath: keyPath] = newValue // change property value
return self
}
}
extension UILabel: Builder { }
let label = UILabel()
.set(\.text, to: "Hello world")
.set(\.textColor, to: .red)
.set(\.font, to: .systemFont(ofSize: 24, weight: .medium))
Note: if you want to apply this to any UIKit
component, just make it conform to the Builder protocol just like we did with the UILabel
like extension extension UIButton: Builder { }
. but if we want to use this on any NSObject or any UIKit element then we can instead make the NSObject conforms to the Builder protocol extension NSObject: Builder { }
.
#keyPath()
syntax limitations available in Swift 3. As you might know #keyPath() is represented using a String which has a number of limitations.#keyPath()
works only with NSObjects