We can use key paths to have a reference to a property from an object. For example, in this simple case, we can pass to a method an object and a reference to a property from that object.
struct SomeObject {
var someInt: Int = 3
var someString: String = "some text"
}
func print<T: CustomStringConvertible>(
property keypath: KeyPath<SomeObject, T>,
from object: SomeObject
) {
let value = object[keyPath: keypath]
print(value)
}
let object = SomeObject()
print(property: \.someInt, from: object)
print(property: \.someString, from: object)
We have different types of key path references. KeyPath is a read-only reference, but we can pass a writable one too:
func setValue<T>(
property keypath: WritableKeyPath<SomeObject, T>,
to object: inout SomeObject,
value: T
) {
object[keyPath: keypath] = value
}
var object = SomeObject()
setValue(property: \.someInt, to: &object, value: 10)
setValue(property: \.someString, to: &object, value: "another text")
print(property: \.someInt, from: object) //10
print(property: \.someString, from: object) // another text
KeyPath
and WritableKeyPath
and used for values types. To mutate the object, we need to pass the value type as inout
. We have another type of KeyPath that we can use for references types ReferenceWritableKeyPath
, with this key path, we know we are passing a reference type and we can mutate directly the object:
class ReferenceObject {
var someInt: Int = 3
var someString: String = "some text"
}
func setValueReference<T>(
property keypath: ReferenceWritableKeyPath<ReferenceObject, T>,
to object: ReferenceObject,
value: T
) {
object[keyPath: keypath] = value
}
let referenceObject = ReferenceObject()
setValueReference(property: \.someInt, to: referenceObject, value: 11)
setValueReference(property: \.someString, to: referenceObject, value: "reference text")
Useful cases
When using key paths, we declare the type of the object and the type of the property that will return the key path. We should try to use Generics for these two types so we can create useful extensions and our methods can be used for any type of object: KeyPath<Object, Value>
Generic method to sorting a sequence by a given KeyPath
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted(by: { $0[keyPath: keyPath] >= $1[keyPath: keyPath]})
}
}
Removing duplicates from a collection of objects, but based on a property
extension Sequence {
func removeDuplicates<T: Hashable>(by keyPath: KeyPath<Element, T>) -> [Element] {
var includedValues = Set<T>()
var result = [Element]()
self.forEach {
let value = $0[keyPath: keyPath]
if !includedValues.contains(value) {
result.append($0)
includedValues.insert(value)
}
}
return result
}
}
let testValues = [
SomeObject(someInt: 2, someString: "two"),
SomeObject(someInt: 2, someString: "another value"),
SomeObject(someInt: 3, someString: "three"),
SomeObject(someInt: 34, someString: "three")
]
let result1 = testValues.removeDuplicates(by: \.someInt)
let result2 = testValues.removeDuplicates(by: \.someString)