この投稿は何?
SwiftにおけるKeyPath
について、基本から解説します。
実行環境
- macOS 12.1
- Xcode 13.2.1
- Swift 5.5
KeyPathとは
要は、「あるデータ型に定義されたプロパティまでの参照(パス)」です。
Swift3時代のString KeyPath
KeyPath
は、Objective-Cの時代から利用されていました。
Swiftでもそれを受け継いでおり、クラスに`objcMembers'属性をマークすることで利用できました。
参照型データの場合
@objcMembers class Kid: NSObject {
dynamic var nickname: String = ""
dynamic var age: Double = 0.0
dynamic var bestFriend: Kid? = nil
dynamic var friends: [Kid] = []
init(nickname: String, age: Double) {
self.nickname = nickname
self.age = age
}
}
// ベン君
let ben = Kid(nickname: "Benji", age: 5.5)
// Kid型のnicknameプロパティへのキーパスを作成
let kidsNameKeyPath = #keyPath(Kid.nickname) // String
// キーパスを使って取得した「ベン君のニックネーム」はAny型になってしまう
let name = ben.value(forKeyPath: kidsNameKeyPath) // value(forKeyPath: String) -> Any
print(name as! String) // Benji
この方法で取得したキーパスは、単純なString
型として保持されます。
そして、String KeyPath
を利用して取得したプロパティ値は、型情報が失われてしまう性質がありました。
Swiftに最適化されたKeyPath
Swift4では、全く新しい方法でKeyPath
を利用できるようになりました。
値型データの場合
struct BirthdayParty {
let celebrant: Kid
var theme: String
var attending: [Kid]
}
var bensParty = BirthdayParty(celebrant: ben, theme: "Construction", attending: [])
let birthdayKid = bensParty[keyPath: \BirthdayParty.celebrant]
bensParty[keyPath: \BirthdayParty.theme] = "Ninja"
let birthdayKidsAgeKeyPath = \BirthdayParty.celebrant.age // キーパスを作成
let birthdayboysAge = bensParty[keyPath: birthdayKidsAgeKeyPath] // 5.5
値型データのKeyPath
では型情報が維持され、よりシンプルに記述されます。
ben
はKid
型インスタンスなので、基点型はKid
であることが推論可能です。
従って、Kid
を省略して以下のように記述できます。
let age = ben[KeyPath: \.age]
同様に、bensParty
はBirthdayParty
型インスタンスなので、以下のコードも略記できます。
bensParty[keyPath: \BirthdayParty.theme] = "Ninja"
bensParty[keyPath: \.theme] = "Ninja" // 型推論による基点型の省略記法
この方法で取得したキーパスは、ReferenceWritableKeyPath
型として保持されます。
そして、取得した値の型情報も維持されます。
クラスのキーパス
クラスでも同じようにして、キーパスを利用できます。
class C {
var p = ""
}
let c = C()
let cpKeyPaty = \C.p // ReferenceWritableKeyPath<C, p>
let cp = c[keyPath: \C.p] // String