KeyPathとはなんぞや?
Swift書いてると、object.property
みたいなアクセスをすることがある。
KeyPathは、その「プロパティへの道」を“値として”扱える機能。
ざっくり言うと、
\User.name
みたいに「User型のnameプロパティ」への道筋を変数として扱えるやつ。
=「データの住所」みたいなもの。
この記事で得られること
- Keypathとは何者なのか?
- どう使うのか?
- どこで刺さるのか?
- どう書くとスッキリ&強くなるのか?
+@で、SwiftUIやViewModel、Modelの状態更新まで応用できるから、
「今まで手動でゴリゴリ書いてたやつ、KeyPathで爆速でできるぜ」ってなるかも。
KeyPathの基本
シンプルな例(\Type.property):
struct User {
let name: String
let age: Int
}
let keyPath = \User.name
let user = User(name: "キーパス太郎", age: 30)
print(user[keyPath: keyPath]) // "キーパス太郎"
ポイント: keyPath
がただの変数(値)であること。
でもこの変数を使えば、User
の中のname
にアクセスできる。
キーワード:型安全 & 再利用性
強み | 説明 |
---|---|
✅ 型安全 | \User.name は KeyPath<User, String> 型。型が合わないとコンパイルエラーで止めてくれる。 |
♻️ 再利用性 | 作ったKeyPathは map や filter 、関数の引数などに何度でも使い回せる。 |
動的プロパティアクセスを安全にやりたい現場で刺さる。
let users = [
User(name: "キーパス太郎", age: 25),
User(name: "キーパス次郎", age: 30)
]
let names = users.map(\.name) // ["キーパス太郎", "キーパス次郎"]
配列やmapでの活用
KeyPathの強さが一番わかりやすいのが、配列操作との組み合わせ。
ループ書かなくても .map(\.name)
でデータを抜けるのが、見た目もスマートで気持ちいい。
struct User {
let name: String
let age: Int
}
let users = [
User(name: "キーパス一郎", age: 25),
User(name: "キーパス二郎", age: 30),
User(name: "キーパス三郎", age: 28)
]
let names = users.map(\.name)
print(names) // ["キーパス一郎", "キーパス二郎", "キーパス三郎"]
.map(\.name)
は map { $0.name }
と同じだが、KeyPathを渡す方が短くて型安全。
後述するように、カスタム関数やDSLでの再利用にも強い。
filterやcontainsとの組み合わせ:
let isAnyUnder30 = users.contains { $0[keyPath: \.age] < 30 }
let filtered = users.filter { $0[keyPath: \.name].hasPrefix("キーパス") }
KeyPathで取り出した値を使って、柔軟なロジックを定義できる。
地味だけど、関数型思考で構造化されたコードが書けるようになる。
WritableKeyPathの活用
Keypathとの違い:
型 | 説明 |
---|---|
KeyPath<T, U> | 読み取り専用 |
WritableKeyPath<T, U> | 読み取り & 書き込み可能 |
プロパティを更新できるとは、つまりこういうこと
struct Counter {
var value: Int
}
let path = \Counter.value // WritableKeyPath<Counter, Int>
var counter = Counter(value: 0)
let path = \Counter.value
counter[keyPath: path] = 10
print(counter.value) // 10
つまり、“プロパティへの参照” を動的に持ちながら値を更新できるということ。
structのプロパティを変える
struct User {
var name: String
var age: Int
}
var user = User(name: "キーパス一郎", age: 29)
user[keyPath: \.name] = "キーパス六郎"
print(user.name) // キーパス六郎
updating拡張での活用 (かなり便利)
構造体はimmutable(不変)だから、プロパティ更新するには新しいインスタンスを作り直す必要がある。
でも毎回 var copy = self
→ copy.xxx = yyy
って書くのはめんどくさい。
それを一発でやってくれる拡張が↓
protocol KeyPathUpdatable {}
extension KeyPathUpdatable {
func updating<T>(_ keyPath: WritableKeyPath<Self, T>, to value: T) -> Self {
var copy = self
copy[keyPath: keyPath] = value
return copy
}
}
// 使用例
struct User: KeyPathUpdatable {
var name: String
var age: Int
}
let original = User(name: "キーパス八郎", age: 29)
let updated = original.updating(\.name, to: "キーパス十郎")
print(updated.name) // キーパス十郎
ポイント:
- 自分自身のコピーを作る
- 指定されたプロパティだけ上書き
- 新しい値を持ったインスタンスを返す
これだけでimmutableな更新がサクッと書ける、とても便利。
状態変更やViewModelとの親和性
@Published var user = User(name: "キーパス八郎", age: 29)
func updateName(_ newName: String) {
user = user.updating(\.name, to: newName)
}
// 複数プロパティ変更にも対応(チェーン可能だから、コードが読みやすくなる。)
let newUser = original
.updating(\.name, to: "キーパス團十郎")
.updating(\.age, to: 30)
print(newUser) // User(name: "キーパス團十郎", age: 30)
状態管理フレームワーク(SwiftUI, The Composable Architecture, etc.)でも使える。
応用Tips & 発展テクニック
SwiftUIとの絡み(.self, .id):
List(users, id: \.id) { user in ... }
// これは KeyPath<User, ID> を使ってるからできる芸当。
// 他にも .onDelete(perform:) で要素を特定したり、\.self を使ってListの識別子にしたりもできる。
カスタム関数で汎用性UP(ジェネリック + KeyPath):
// あるプロパティが特定条件に合う要素を抽出したいとき
func filterBy<T, V: Equatable>(
_ keyPath: KeyPath<T, V>,
equals value: V,
in array: [T]
) -> [T] {
array.filter { $0[keyPath: keyPath] == value }
}
// 使い方:
let result = filterBy(\User.age, equals: 30, in: users)
まとめ
SwiftのKeyPath、最初はとっつきにくいけど、使いこなせば実務でガンガン活躍するテクニックになりえる。
💪 メリット/特徴 | 📍 シーン・用途 | 🛠️ 使い方例 |
---|---|---|
✅ 型安全 | プロパティアクセス | let keyPath: KeyPath<User, String> = \User.name |
♻️ 再利用性 | map / filter でのデータ抽出 | users.map(\.name) / users.filter { $0[keyPath: \.age] > 20 } |
🧼 読みやすさ | クロージャの簡略化 | users.sorted { $0[keyPath: \.age] < $1[keyPath: \.age] } |
🔧 拡張性(WritableKeyPath) | 構造体の状態更新 / ViewModelの書き換え | user.updating(\.name, to: "Mega Bro") |
🧠 柔軟性 | SwiftUI / フォームバリデーション | List(users, id: \.id) / [(\User.name, "名前が必須です")] |
🔄 汎用的な抽象化 | ジェネリック関数にKeyPathを渡す | filterBy(\User.age, equals: 30, in: users) |