先日 Swift5 がリリースされました! ABI 安定(Application Binary Interface)、 Result 型の追加など色々ありましたが、その中で identity keypath という KeyPath の発表がありました。

また3月に開催された try!Swift2019 でも KeyPath についてのセッションがあって気になっていたところでした。
今回はその KeyPath についていろいろ調べて見ました。

TL;DR

  • Swift3で #KeyPath が追加
    • KVC 、 KVO などのキー指定をコンパイル時にチェック可能になった
  • Swift4で KeyPath が追加
    • Swift3 #KeyPath 問題点の解決
    • 違う要素を持つ Struct を抽象化できる
    • KeyPath には種類がある

#KeyPath

  • Swift3で利用可能
  • KVC 、 KVO などのキー指定をコンパイル時にチェックできる(typo を防げる)
  • コンパイル後、文字リテラルに置換される
    そのため、 Swift3以降でも Swift2以前と同じく文字列でキーの指定が可能

    class Animal: NSObject {
        @objc dynamic var name: String
        init(name: String) {
            self.name = name
        }
    }
    
    let animal = Animal(name: "Dog")
    
    // Swift3 #KeyPath での書き方
    animal.setValue("Cat", forKey: #keyPath(Animal.name))
    animal.value(forKey: #keyPath(Animal.name))
    
    let keyPath = #keyPath(Animal.name)
    print(type(of: keyPath))        // String
    
    // 以前の書き方(Swift3以降でもこの書き方はできる)
    animal.setValue("Horse", forKey: "name")
    animal.value(forKey: "name")    
    

問題点

  • 不必要に解析が遅い
  • NSObjects の継承が必要
  • Darwin プラットフォームでしか使用できない
  • 型情報が失われる(Any になる)
    想定外の型の値を渡してもコンパイルエラーにならず、実行時エラーになる

    type(of: animal.value(forKey: #keyPath(Animal.name)))   // Optional<Any>.Type
    animal.setValue(0, forKey: #keyPath(Animal.name))       // 実行時エラー
    

    Smart KeyPaths: Better Key-Value Coding for Swift

KeyPath

  • Swift4 で利用可能
  • Swift3の #keyPath の問題点を解決するため追加された
  • 動的にプロパティにアクセスするために使用する
  • Protocol では出来ない、違う要素を持つ Struct を抽象化できる

    struct User {
        var username: String
    }
    
    var player = User(username: "Mike")
    
    let keyPath = \User.username        // \が KeyPath を生成する式です
    player[keyPath: keyPath] = "Becky"
    

KeyPath Composition

  • 構造体の下の階層には、以下のようにつなげて書くことでアクセスできます。

    let keyPath = \User.address
    let newKeyPath = keyPath.appending(path: \Address.country)
    // 別の書き方
    let newKeyPath2 = (\User.address).appending(path: \Address.country)
    

Hashable

KeyPath の種類

KeyPath は以下の階層になっています。

AnyKeyPath
    ↓
PartialKeyPath<Root>
    ↓
KeyPath<Root, Value>
    ↓
WritableKeyPath<Root, Value>
    ↓
ReferenceWritableKeyPath<Root, Value>

AnyKeyPath

  • Read Only
  • Root,Value はコンパイル時に特定されない
  • 実行時に具体的な型に解決

    struct Address {
        let country: String
    }
    struct User {
        let username: String
        let age: Int
        let address: Address
    }
    
    var user = User(username: "Mike", age: 21, address: Address(country: "Japan"))
    let keyPath: AnyKeyPath = \User.username                // Optional<Any>
    
    user[keyPath: keyPath] = "太郎"   // Read Only なのでコンパイルエラー
    

    AnyKeyPath

PartialKeyPath<Root>

  • Read Only
  • Root はコンパイルで型が特定
  • Value は実行時に具体的な型に解決
  • 異なる種類の KeyPath を配列にまとめることもできる

    let partialKeyPaths: [PartialKeyPath<User>] = [
        \User.username, \User.address
    ]
    

    PartialKeyPath

KeyPath<Root, Value>

  • Read Only
  • Root,Value もコンパイルで型が特定

    KeyPath

WritableKeyPath<Root, Value>

  • Root,Value もコンパイルで型が特定
  • KeyPath のサブクラス

    WritableKeyPath

ReferenceWritableKeyPath<Root, Value>

  • Root,Value もコンパイルで型が特定
  • プロパティが class にある場合、作成される
  • WritableKeyPath のサブクラス

    ReferenceWritableKeyPath

KeyPath の便利な使い方

ソート機能

通常は Sort 関数で以下のように書くことが多いと思います。

var user = User(username: "Mike", age: 21, address: Address(country: "Japan"))
var user2 = User(username: "Charlie", age: 17, address: Address(country: "Korea"))
var user3 = User(username: "Tommy", age: 24, address: Address(country: "Canada"))

var people = [user, user2, user3]
people.sort{ $0.username > $1.username }
//people.sort{ $0.address.country > $1.address.country }

KeyPath を使用すると以下のようにかけます。

extension Sequence {
    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        return sorted { first, second in
            return first[keyPath: keyPath] > second[keyPath: keyPath]
        }
    }
}

let newPeople = people.sorted(by: \User.username)
//let newPeople = people.sorted(by: \User.address.country)

異なるモデルの UITableView 表示

タイトル、サブタイトル、画像表示など、 UITableView で比較的多く見られるパターンの時、 KeyPath を利用して汎用的な CellConfigurator を作成しておけば、異なるモデルでも対応できる。

struct CellConfigurator<Model> {
    let titleKeyPath: KeyPath<Model, String>
    let subtitleKeyPath: KeyPath<Model, String>
    let imageKeyPath: KeyPath<Model, UIImage?>

    func configure(_ cell: UITableViewCell, for model: Model) {
        cell.textLabel?.text = model[keyPath: titleKeyPath]
        cell.detailTextLabel?.text = model[keyPath: subtitleKeyPath]
        cell.imageView?.image = model[keyPath: imageKeyPath]
    }
}

let songCellConfigurator = CellConfigurator<Song>(
    titleKeyPath: \.name,
    subtitleKeyPath: \.artistName,
    imageKeyPath: \.albumArtwork
)

let playlistCellConfigurator = CellConfigurator<Playlist>(
    titleKeyPath: \.title,
    subtitleKeyPath: \.authorName,
    imageKeyPath: \.artwork
)

The power of key paths in Swift

Swift5 で追加された機能

identity keypath

  • 型自身(.self)の Keypath の取得が可能
  • KeyPath を通して型のすべての値を取得可能
  • observer と組み合わせることで、型のすべての値を使って state の値を更新できる

    class ValueController<T> { 
        private var state: T
        private var observers: [(T) -> ()]
    
        subscript<U>(key: WritableKeyPath<T, U>) {
            get { return state[keyPath: key] }
            set {
                state[keyPath: key] = newValue
                for observer in observers {
                    observer(state)
                }
            }
        }
    }
    

Identity key path

まとめ

KeyPath を紹介して来ましたが、いかがだったでしょうか?
Swift3で #KeyPath が、 Swift4で KeyPath が実装され、よりシンプルかつ Swift らしいものになっています。 今まであまり使う機会なかったのですが、 KeyPath を使うことで、抽象的に値を利用することができ色々と使用できる場面はありそうです。今後積極的に使っていきたいと思います!

参考文献