#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>
AnyKeyPath
とPartialKeyPath<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