SwiftのKeyPathと戯れる

# `KeyPath`とは?

---

## `KeyPath` とは?
Swift4で追加された、プロパティに動的にアクセスするための記法

> Use key-path expressions to access properties dynamically.[^1]

[^1]: https://developer.apple.com/documentation/swift/swift_standard_library/key-path_expressions

---

## Swift3.xまでの `#keyPath`

```swift
class Hoge: NSObject { // 🤔
    @objc var foo: String // 🤔
    let bar: String

    init(foo: String, bar: String) {
        self.foo = foo
        self.bar = bar
    }
}

let foo = hoge.value(forKeyPath: #keyPath(Hoge.foo))
print(type(of: foo)) // Optional<Any>

print(#keyPath(hoge.bar)) // error: argument of '#keyPath' refers to non-'@objc' property 'bar'
```

---

#### Obj-C時代からあるKVOでのtypoを防ぐ程度の役割しかない

- `NSObject` にしか使えない
- `@objc` 宣言されたプロパティのみ
- 返却値は`Any!` (型情報が消える😫)
- 解析が遅い、Darwinでしか使えない[^2]といった問題点も

[^2]: https://github.com/apple/swift-evolution/blob/master/proposals/0161-key-paths.md#we-can-do-better-than-string

--- 

## Swift4からの `KeyPath`

- Swiftの強力な型システムに基づいた機構
    - structにもenumにも使える
    - 型情報が維持される
    - Swiftのstdlibに含まれているのでプラットフォームに依存しない

--- 

## `NSObject`も`@objc`宣言もいらない

```swift
class Hoge {
    var foo: String
    let bar: String
}
let hogeFoo: ReferenceWritableKeyPath<Hoge, String> = \.foo
let hogeBar = \Hoge.bar // KeyPath<Hoge, String>

let hoge = Hoge(foo: "foo", bar: "bar")
print(hoge.foo) // foo
hoge[keyPath: hogeFoo] = "hoge-foo"
print(hoge.foo) // hoge-foo
hoge[keyPath: hogeBar] = "bar" // error: cannot assign through subscript: 'hogeBar' is a read-only key path
```

--- 

## structにも使える

```swift
struct Fuga {
    var foo: String
    let bar: String
}
let fugaFoo: WritableKeyPath<Fuga, String> = \.foo
let fugaBar = \Fuga.bar // KeyPath<Fuga, String>

var fuga = Fuga(foo: "foo", bar: "bar")
print(fuga.foo) // foo
fuga[keyPath: fugaFoo] = "fuga-foo"
print(fuga.foo) // fuga-foo
fuga[keyPath: fugaBar] = "bar" // error: cannot assign through subscript: 'fugaBar' is a read-only key path
```

--- 

## `KeyPath`関連クラス

```
_AppendKeyPath
└ AnyKeyPath
  └ PartialKeyPath<Root>
    └ KeyPath<Root, Value>
      └ WritableKeyPath<Root, Value> // Value is variable
        └ ReferenceWritableKeyPath<Root, Value> // Root is class
```

--- 

# 小ネタ

--- 

## Tupleも`Root`になれる

```swift
let hogeFuga = (hoge, fuga)

let hogeKeyPath = \(Hoge, Fuga).0 // no label
let fugaKeyPath = \(hoge: Hoge, fuga: Fuga).fuga // with labels

hogeFuga[keyPath: hogeKeyPath] // hoge
hogeFuga[keyPath: fugaKeyPath] // fuga
```

--- 

## プロパティのネストも可能

```swift
struct FooBar {
    var hoge: Hoge
    var fuga: Fuga
}

let fooBarHogeFoo = (\FooBar.hoge).appending(path: \Hoge.foo)
\FooBar.hoge.foo == fooBarHogeFoo // true

print(fooBar.hoge.foo) // hoge-foo
fooBar[keyPath: fooBarHogeFoo] = "foo-bar-hoge-foo"
print(fooBar.hoge.foo) // foo-bar-hoge-foo
fooBar[keyPath: \.hoge.foo] = "foo"
print(fooBar.hoge.foo) // foo

let fooBarFugaFoo = (\FooBar.fuga).appending(path: \Fuga.foo)
fooBar[keyPath: fooBarFugaFoo] = "foo-bar-fuga-foo" // error: cannot assign through subscript: 'fooBar' is a 'let' constant
```

---

# `KeyPath`の実用例

---

## プロパティでソートする[^3]

```swift
public extension Sequence {
    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }

    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T?>) -> [Element] {
        sorted {
            guard let l = $0[keyPath: keyPath],
                let r = $1[keyPath: keyPath] else { return false }
            return l < r
        }
    }

    func min<T: Comparable>(by keyPath: KeyPath<Element, T>) -> Element? {
        self.min { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }
}

struct Person {
    var id: Int
    var name: String
    var age: Int
}

extension Person: CustomStringConvertible {
    var description: String { name }
}

let people: [Person] = [
    .init(id: 3, name: "Bob", age: 28),
    .init(id: 1, name: "Emma", age: 40),
    .init(id: 4, name: "Amelia", age: 18),
    .init(id: 2, name: "George", age: 22),
]

// Before
people.sorted(by: { $0.id < $1.id })      // ["Emma", "George", "Bob", "Amelia"]
people.sorted(by: { $0.name < $1.name })  // ["Amelia", "Bob", "Emma", "George"]

// After
people.sorted(by: \.id)   // ["Emma", "George", "Bob", "Amelia"]
people.sorted(by: \.name) // ["Amelia", "Bob", "Emma", "George"]
```

[^3]: https://qiita.com/417_72ki/items/de4a7e49c32d39d3bfb6

---

## 同じAnchor同士へのConstraintを貼る

```swift
public extension UIView {
    func equal<Axis, Anchor: NSLayoutAnchor<Axis>>(_ anchor: KeyPath<UIView, Anchor>,
                                                   to target: UIView) -> NSLayoutConstraint {
        self[keyPath: anchor].constraint(equalTo: target[keyPath: anchor])
    }

    func equal<Axis, Anchor: NSLayoutAnchor<Axis>>(_ anchor: KeyPath<UIView, Anchor>,
                                                   to target: UIView,
                                                   constant: CGFloat) -> NSLayoutConstraint {
        self[keyPath: anchor].constraint(equalTo: target[keyPath: anchor], constant: constant)
    }
}

// Before
NSLayoutConstraint.activate([
    view.topAnchor.constraint(equalTo: target.topAnchor),
    view.leadingAnchor.constraint(equalTo: target.leadingAnchor),
    view.trailingAnchor.constraint(equalTo: target.trailingAnchor),
    view.bottomAnchor.constraint(equalTo: target.bottomAnchor)
])

// After
NSLayoutConstraint.activate([
    view.equal(\.topAnchor, to: target),
    view.equal(\.bottomAnchor, to: target),
    view.equal(\.leadingAnchor, to: target),
    view.equal(\.trailingAnchor, to: target)
])

```

---

更に関数チックに書いた例
https://www.objc.io/blog/2018/10/30/auto-layout-with-key-paths/

--- 

# Key Path Expressions as Functions

--- 

## Key Path Expressions as Functions

- Swift5.2で追加([SE-0249](https://github.com/apple/swift-evolution/blob/master/proposals/0249-key-path-literal-function-expressions.md))
- `KeyPath` 表現をクロージャを引数とする関数に渡すことができる
    - コレクション操作などが簡潔に書けるように
- Xcode12.xならコード補完が効く

---

### 例

```swift
struct Department {
    var name: String
    var member: [Person]
}

extension Department {
    var boss: Person {
        member.min(by: \.id)!
    }
}

let departments: [Department] = [
    .init(name: "General Affairs Department",
          member: [
            .init(id: 3, name: "Bob", age: 28),
            .init(id: 1, name: "Emma", age: 40)
    ]),
    .init(name: "Development Department",
          member: [
            .init(id: 4, name: "Amelia", age: 18),
            .init(id: 2, name: "George", age: 22)
    ])
]
```

---

### Before

```swift
departments.flatMap { $0.member }
    .map { $0.name } // ["Bob", "Emma", "Amelia", "George"]
departments.map { $0.boss }
    .map { $0.name } // ["Emma", "George"]
departments.map { $0.boss.name }
```

---

### After

```swift
departments.flatMap(\.member)
    .map(\.name) // ["Bob", "Emma", "Amelia", "George"]
departments.flatMap(\.member.name) // Value of type '[Person]' has no member 'name'
departments.map(\.boss)
    .map(\.name) // ["Emma", "George"]
departments.map(\.boss.name) // ["Emma", "George"]
```

---

#### Performance[^4]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135304/3ed580f1-f6c1-3627-dae6-b02688c8dbbc.png)

```swift
extension Person {
    var canDrink: Bool { age >= 20 }
}
```

[^4]: [measure関数](https://github.com/417-72KI/XcodePlaygroundSample/blob/master/PlaygroundSample.playground/Sources/Measure.swift)

---

### 注意

変数/定数化したKeyPathを渡すことはできない(2020/12/9現在)

```swift
let bossKeyPath = \Department.boss
let nameKeyPath = \Person.name
let bossNameKeyPath = bossKeyPath.appending(path: nameKeyPath)

departments.map(bossKeyPath) // Cannot convert value of type 'KeyPath<Department, Person>' to expected argument type '(Department) throws -> T'
    .map(nameKeyPath) // Cannot convert value of type 'WritableKeyPath<Person, String>' to expected argument type '(T) throws -> T'

departments.map(bossNameKeyPath) // Cannot convert value of type 'KeyPath<Department, String>' to expected argument type '(Department) throws -> T'
```

---

# 最後に
- `KeyPath`によってSwiftコードがより簡潔に書きやすくなった
- RxSwift / Combine のストリーム変換にも使えるのでより宣言的に書ける
- Genericsと併用することで処理の汎用化も大幅に進みそう
- [Sample in Xcode Playground](https://github.com/417-72KI/XcodePlaygroundSample/blob/master/PlaygroundSample.playground/Pages/KeyPath.xcplaygroundpage/Contents.swift)