Brandon Bradley
Brandon Bradley

Reputation: 3398

What is a KeyPath used for?

In Swift 4 many on the Foundation team have discussed how much easier it is to use keyPaths as compared to Swift 3. This begs the question... What is a keyPath? Seriously, I can't find any clear resources.

Upvotes: 16

Views: 5990

Answers (2)

yoAlex5
yoAlex5

Reputation: 34311

Swift Key Value Coding (KVC)

Official doc - Key-Path Expressions

[Objective-C Key Value Coding(KVC) vs Key Value Observing(KVO)]

KeyPath(key path) is a reference to a property of a type(rather than value). It adds a dynamism into language

There are some of them:

  • KeyPath<Root, Value> - read only
  • WritableKeyPath<Root, Value> - read/write for var property
  • ReferenceWritableKeyPath<Root, Value> - read/write only for reference types[About]
  • PartialKeyPath<Root> - read only, Value is any
  • AnyKeyPath - read only, Root is any, Value is any

KeyPath consists of next main parts: Value Path, Root Type, Value Type(property Type)

//Value Path is SomeClass.someVariable.count
//Root Type is SomeClass
//Value Type is Int as a result of .count
let customKeyPath: KeyPath<SomeClass, Int> = \SomeClass.someVariable.count

//read
let count1 = someClass1[keyPath: customKeyPath]
let count2 = someClass2[keyPath: customKeyPath]

You can find that KeyPath is used for shortcuts(e.g. sorting, iterating, filtering), KVO[Example], MemoryLayout[Example], SwiftUI and others more advanced features. #keyPath is used in animation for example let colorsAnimation = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.colors))

examples:

let arr: [SomeClass] = []

//sort from iOS v15
let result1 = arr.sorted(using: KeyPathComparator(\.someVariable, order: .reverse))

//map
let result2 = arr.map(\.someVariable)
//instead of arr.map { $0.someVariable }

Syntax:

class SomeClass {
    @objc
    var someVariable: String = "Hello World!"
}

let keyPath1: String = #keyPath(SomeClass.someVariable)
assert(keyPath1 == "someVariable")

//let keyPath2: KeyPath<SomeClass, String> = \SomeClass.someVariable
//let keyPath2: WritableKeyPath<SomeClass, String> = \SomeClass.someVariable
let keyPath2: ReferenceWritableKeyPath<SomeClass, String> = \SomeClass.someVariable
//let keyPath2: PartialKeyPath<SomeClass> = \SomeClass.someVariable
//let keyPath2: AnyKeyPath = \SomeClass.someVariable


let someClass = SomeClass()

//read
let res = someClass[keyPath: \SomeClass.someVariable] //or just \.v(from context)
//or
//let res = someClass[keyPath: keyPath2]
assert(res == "Hello World!")

//write only for writable KeyPath
//or you get build-time error:Cannot assign through subscript: 'keyPath2' is a read-only key path
someClass[keyPath: \SomeClass.someVariable] = "world is changed 1" //or just \.v(from context).
assert(someClass.someVariable == "world is changed 1")

someClass[keyPath: keyPath2] = "world is changed 2"
assert(someClass.someVariable == "world is changed 2")


Pre Swift v4 - slow and not type safe
//read
let res = someClass.value(forKeyPath: keyPath) //or forKey:

//write
someClass.setValue("Another string", forKeyPath: keyPath) //or forKey:
  • Starts from backwards slash \ (backslash)
let someKeyPath = \SomeClass.someVariable //KeyPath<SomeClass, String>
  • If there is a known object you can use \. (backslash dot)
someClass.observe(\.someVariable, options: .new) //someClass1.someVariable

Upvotes: 1

Durul Dalkanat
Durul Dalkanat

Reputation: 7445

Objective-C has the ability to reference a property dynamically rather than directly. These references, called keypaths. They are distinct from direct property accesses because they don't actually read or write the value, they just stash it away for use.

Let define a struct called Cavaliers and a struct called Player, then create one instance of each:

// an example struct
struct Player {
    var name: String
    var rank: String
}

// another example struct, this time with a method
struct Cavaliers {
    var name: String
    var maxPoint: Double
    var captain: Player

    func goTomaxPoint() {
        print("\(name) is now travelling at warp \(maxPoint)")
    }
}

// create instances of those two structs
let james = Player(name: "Lebron", rank: "Captain")
let irving = Cavaliers(name: "Kyrie", maxPoint: 9.975, captain: james)

// grab a reference to the `goTomaxPoint()` method
let score = irving.goTomaxPoint

// call that reference
score()

The last lines create a reference to the goTomaxPoint() method called score. The problem is, we can't create a reference to the captain's name property but keypath can do.

let nameKeyPath = \Cavaliers.name
let maxPointKeyPath = \Cavaliers.maxPoint
let captainName = \Cavaliers.captain.name
let cavaliersName = irving[keyPath: nameKeyPath]
let cavaliersMaxPoint = irving[keyPath: maxPointKeyPath]
let cavaliersNameCaptain = irving[keyPath: captainName]

Please test with Xcode 9 or capable snapshot.

Upvotes: 19

Related Questions