Swift
SwiftDay 25

#keyPathとKeyPath

#keyPath - Key-Path String Expression

Swift 3で #keyPath が追加され、KVCなどのキー指定をコンパイル時にチェックできるようになりました。
これを使うことでキー値を文字列で指定することによるtypoを防ぎ、リファクタリングにも追従できるようになります。

  • Swift 3
  class Person: NSObject {
      dynamic var name: String

      init(name: String) {
          self.name = name
      }
  }

  let person = Person(name: "hoge")

  #keyPath(Person.name)  // "name"

  // KVC
  person.setValue("fuga", forKey: #keyPath(Person.name))
  person.value(forKey: #keyPath(Person.name))  // "fuga"
  • Swift 2以前
  person.setValue("fuga", forKey: "name")
  person.valueForKey("name")  // "fuga"

#keyPath では何かしら型が用意されているわけではなく、コンパイル後は文字列リテラルに置き換えられます。
そのためSwift 3以降でもSwift 2以前と同じく文字列でキーの指定ができます。

  • Swift 3
  // 今まで通り文字列でキー値の指定もできる。結果は#keyPathを使った場合と同じ。
  person.value(forKey: "name") // fuga

#keyPathの問題点

#keyPathによってtypoなどの問題は防げるようになったのですが、
その一方でKVCやKVOにおける値の操作はAnyを通して行うままなので片手落ちな感じです。

例えばKVCでString型の値を渡すべきところにInt型の値を渡してもコンパイラは何も感知しません。

type(of: person.value(forKey: #keyPath(Person.name))) // Optional<Any>.Type

person.setValue(0, forKey: #keyPath(Person.name)) // 実行時エラー

SE-0161で挙げられている問題もまとめると#keyPathには以下のような問題があります。

  • 型情報の喪失
  • 無駄に構文解析が遅くなる
  • NSObjectにしか使えない
  • Darwinプラットフォームのみ対応

KeyPath - Key-Path Expression

#keyPathの問題点を解決するため、Swift 4で KeyPath が追加されました。

  • Swift 4
  class Person {
      var name: String
      var age: Int = 0

      init(name: String) {
          self.name = name
      }
  }

  let person = Person(name: "hoge")

  person[keyPath: \Person.name] = "fuga"

  person[keyPath: \Person.name] // "fuga"

型情報が維持されているため#keyPathで片手落ちだった部分が解消されています。

type(of: person[keyPath: \Person.name]) // String.Type
person[keyPath: \Person.name] = 0 // コンパイルエラー

KeyPathを使ったKVO

KeyPathの追加に合わせてKVOも新しい方法が追加されています。
こちらはObjective-Cランタイムに依存しているので、
利用できるのはNSObjectを継承しているクラスの@objc dynamic varなメンバのみです。

例えばWKWebViewの進捗estimatedProgressを監視するコードは下記のようになります。

import UIKit
import WebKit

class ViewController: UIViewController {
    let webView = WKWebView()

    var observation: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()

        observation = webView.observe(\.estimatedProgress, options: .new) { webView, change in
            guard let progress = change.newValue else {
                return
            }
            print(progress)
        }

        webView.load(URLRequest(url: URL(string: "https://google.com")!))
    }
}

Swift 3以前は addObserver で監視対象を登録して変更は全てobserveValue で受け取る形でしたが、
処理をクロージャで書けるようになって随分使いやすくなりました。
もちろんクロージャに渡される通知元と監視している値の両方とも型が消されていないので安心して使用できます。

KeyPathの実装を少しだけ見てみる

せっかくなので実装についても少しだけ見てみます。

\Person.nameの型を調べてみるとReferenceWritableKeyPath<Person, String>となっています。
これを辿っていくとざっくり下記のような継承関係になっていることがわかります。
(※辿らなくてもSE-0161に書いてあります)

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

AnyKeyPathPartialKeyPath<Root>は型安全でないObjective-C APIとの繋ぎのためにあるようです。

PartialKeyPath<Root>があるのでキーをまとめることができます。

let keys = [
    \Person.name,
    \Person.age
]


let values = keys.map { person[keyPath: $0] } // ただし [Any] になる

KeyPathの実用的な事例としてはkishikawakatsumiさんのkishikawakatsumi/Kueryがあります。

参考

SE-0062 Referencing Objective-C key-paths
SE-0161 Smart KeyPaths: Better Key-Value Coding for Swift
kishikawakatsumi/Kuery