KeyPath

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)