【Swift】Key-Pathについて

# はじめに

SwiftのKeyPathについて理解できてなく、一度公式ドキュメントの内容をまとめてみました。

https://docs.swift.org/swift-book/ReferenceManual/Expressions.html#ID563

# 内容

### キーパス式

キーパス式は、ある型のプロパティまたは添え字を指します。

\ `type name` . `path`

`type name`には、String, [Int], Set<Int> などの汎用パラメータも含まれます。

パスは、プロパティ名、添え字、オプション連結式、強制アンラップ式から構成されます。

コンパイル時に、キーパス式は KeyPath クラスのインスタンスに置き換えられます。
[`class KeyPath<Root, Value>`](https://developer.apple.com/documentation/swift/keypath)(特定のルート型から特定の結果の値型へのキーパス)

キーパスを使用して値にアクセスするには、すべての型で利用可能な `[keyPath: ]` にキーパスを渡します。

```swift
struct SomeStructure {
    var someValue: Int
}

let s = SomeStructure(someValue: 12)
let pathToProperty = \SomeStructure.someValue

let value = s[keyPath: pathToProperty]
// value is 12
```

型推論で暗黙の型を決定できるコンテキストでは、型名を省略することができます。(👇`\SomeClass.someProperty` の代わりに、 `\.someProperty` を使用しています）

```swift
class SomeClass: NSObject {
    @objc dynamic var someProperty: Int
    init(someProperty: Int) {
        self.someProperty = someProperty
    }
}

let c = SomeClass(someProperty: 10)
c.observe(\.someProperty) { object, change in
    // ...
}
```

パスは self を参照して ID キーパス (\.self) を作成することができます。IDキーパスはインスタンス全体を参照するので、変数に格納されている全てのデータに一度にアクセスできます。

```swift
var compoundValue = (a: 1, b: 2)
// Equivalent to compoundValue = (a: 10, b: 20)
compoundValue[keyPath: \.self] = (a: 10, b: 20)
```

パスは、ピリオドで区切られた複数のプロパティ名を含むことができます。このコードでは、キーパス式 `\OuterStructure.outer.someValue` を使用して、OuterStructure の outer プロパティの someValue プロパティにアクセスします。

```swift
struct OuterStructure {
    var outer: SomeStructure
    init(someValue: Int) {
        self.outer = SomeStructure(someValue: someValue)
    }
}

let nested = OuterStructure(someValue: 24)
let nestedKeyPath = \OuterStructure.outer.someValue

let nestedValue = nested[keyPath: nestedKeyPath]
// nestedValue is 24
```

型が`Hashable`プロトコルに準拠している限り、キーパスを使って要素にアクセスできます。

```swift
let greetings = ["hello", "hola", "bonjour", "안녕"]
let myGreeting = greetings[keyPath: \[String].[1]]
// myGreeting is 'hola'
```

キー パスのコンポーネントを組み合わせて、型内で深くネストされた値にアクセスできます。

```swift
let interestingNumbers = ["prime": [2, 3, 5, 7, 11, 13, 17],
                          "triangular": [1, 3, 6, 10, 15, 21, 28],
                          "hexagonal": [1, 6, 15, 28, 45, 66, 91]]
print(interestingNumbers[keyPath: \[String: [Int]].["prime"]] as Any)
// Prints "Optional([2, 3, 5, 7, 11, 13, 17])"
print(interestingNumbers[keyPath: \[String: [Int]].["prime"]![0]])
// Prints "2"
print(interestingNumbers[keyPath: \[String: [Int]].["hexagonal"]!.count])
// Prints "7"
print(interestingNumbers[keyPath: \[String: [Int]].["hexagonal"]!.count.bitWidth])
// Prints "64"
```

通常は関数やクロージャを提供するようなコンテキストで、キーパス式を使用することができます。具体的には、ルート型が SomeType で、そのパスが Value 型の値を生成するキーパス式を、 (SomeType) -> Value 型の関数またはクロージャの代わりに使用することができます。

```swift
struct Task {
    var description: String
    var completed: Bool
}
var toDoList = [
    Task(description: "Practice ping-pong.", completed: false),
    Task(description: "Buy a pirate costume.", completed: true),
    Task(description: "Visit Boston in the Fall.", completed: false),
]

// Both approaches below are equivalent.
let descriptions = toDoList.filter(\.completed).map(\.description)
let descriptions2 = toDoList.filter { $0.completed }.map { $0.description }
```

キーパス式の副作用は、その式が評価される時点でのみ評価されます。たとえば、キーパス式の添え字の内部で関数を呼び出すと、キーパスが使用されるたびに呼び出されるのではなく、式の評価の一部として一度だけ関数が呼び出されます。

```swift
func makeIndex() -> Int {
    print("Made an index")
    return 0
}
// The line below calls makeIndex().
let taskKeyPath = \[Task][makeIndex()]
// Prints "Made an index"

// Using taskKeyPath doesn't call makeIndex() again.
let someTask = toDoList[keyPath: taskKeyPath]
```

### キーパス文字列式

キーパス文字列式は、Objective-Cのプロパティを参照するための文字列にアクセスし、キーバリュー・コーディングやキーバリュー観察APIで使用することができます。

`#keyPath(property name)`

プロパティ名は、Objective-Cランタイムで利用可能なプロパティへの参照でなければならない。コンパイル時、キーパスの文字列式は文字列リテラルに置き換えられる。


```swift
class SomeClass: NSObject {
    @objc var someProperty: Int
    init(someProperty: Int) {
        self.someProperty = someProperty
    }
}

let c = SomeClass(someProperty: 12)
let keyPath = #keyPath(SomeClass.someProperty)

if let value = c.value(forKey: keyPath) {
    print(value)
}
// Prints "12"
```

# おわりに

SwiftUIで`\.`をあまり理解せずに使っていたので、今回確認したことでスッキリしました！

# 参考

https://docs.swift.org/swift-book/ReferenceManual/Expressions.html#ID563

https://docs.swift.org/swift-book/ReferenceManual/Expressions.html#ID549

https://llcc.hatenablog.com/entry/2020/03/30/000000