[Xcode/Swift] SwiftのKeyPathを理解する

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.nameKeyPath<User, String> 型。型が合わないとコンパイルエラーで止めてくれる。
♻️ 再利用性作ったKeyPathは mapfilter、関数の引数などに何度でも使い回せる

動的プロパティアクセスを安全にやりたい現場で刺さる。

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 = selfcopy.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) // キーパス十郎

ポイント:

  1. 自分自身のコピーを作る
  2. 指定されたプロパティだけ上書き
  3. 新しい値を持ったインスタンスを返す

これだけで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)