【Swift】KeyPathについて基本から応用まで解説!

Swift

こんにちは。新人プログラマーの岩本です。

今回はKeyPathについて、自分なりに調べたことをまとめたいと思います。

KeyPathの基本から応用の仕方まで紹介するので、ぜひ最後までご覧ください。

この記事は新卒エンジニアが執筆しています。
そのため内容に間違いや不備がある場合があります。
もし間違いを発見しましたら、どんどん指摘していただけると幸いです。

実装環境

  • 言語…Swift 5.8.1
  • OS… macOS Ventura 13.1
  • アプリ… Xcode 14.3.1

KeyPathとは

公式ドキュメントでは以下のように説明されています。

A key path from a specific root type to a specific resulting value type.

https://developer.apple.com/documentation/swift/keypath

日本語に訳すと、こんな感じです。

特定のルート型から特定の結果の値型へのキーパス

すごく簡単に言うと「データ型に定義されたプロパティまでの道のり」です。

定義は以下のようにされています。

Swift
class KeyPath<Root, Value>

詳しくは後で解説しますが、Rootが対象のデータ型で、Valueが指定したいプロパティの型です。

ではここから簡単な使用例を解説します。

基本の使用例

以下のような構造体を用意します。

Swift
struct Person {
	let name: String
	let age: Int
}

let person = Person(name: "Ryuto", age: 20)

ここでPersonのnameまでの道のりを持ったKeyPathを宣言します。

Swift
let keyPath: KeyPath<Person, String> = \.name

このKeyPathを使えば、以下のようにnameプロパティを参照することができます。

Swift
print(person[keyPath: keyPath]) // "Ryuto"

KeyPathを使えばオプショナル型のアンラップも以下のように書くことができます。

Swift
struct Person {
    let fullName: String?
}

let person = Person(fullName: "AAA BBB")

// 一般的なアンラップ
if let fullName = person.fullName {
    print(fullName)
}

// KeyPathを使ったアンラップ
let fullNamePath: KeyPath<Person, String?> = \.fullName
if let fullName = person[keyPath: fullNamePath] {
    print(fullName)
}

KeyPathを使用するメリット

KeyPathを使用するメリットの1つは「値を動的に受け取ることができる」ことです。

たとえばCGSize型を引数に取り、その幅と高さのどちらかを返す関数を作りたいとします。

まずはKeyPathを使わずに書いてみます。

Swift
func getWidth(_ cgSize: CGSize) -> CGFloat {
    return cgSize.width
}
func getHeight(_ cgSize: CGSize) -> CGFloat {
    return cgSize.height
}

let cgSize = CGSize(width: 50, height: 100)

print(getWidth(cgSize))  // 50.0
print(getHeight(cgSize)) // 100.0

これはこれでいいですが、少し冗長な感じがします。

ここでKeyPathを使うことで、関数を1つにまとめることができます。

Swift
func getSize(_ cgSize: CGSize, path: KeyPath<CGSize, CGFloat>) -> CGFloat {
    return cgSize[keyPath: path]
}

let cgSize = CGSize(width: 50, height: 100)

print(getSize(cgSize, path: \.width))  // 50.0
print(getSize(cgSize, path: \.height)) // 100.0

引数でwidthまたはheightまでの道のりを指定することで、どちらの値を取ることもできるようになりました。

【ステップアップ】KeyPathを使った応用例

ではここからはKeyPathを使った応用例を紹介します。

Viewの拡張関数として、幅と高さを取得するreadSizeを定義します。

View++.swift
public extension View {
    func readSize<T: Equatable>(
        of keyPath: KeyPath<CGSize, T> = \.self,
        onChange: @escaping (T) -> Void
    ) -> some View {
        background {
            GeometryReader { geometry in
                Color.clear
                    .onAppear { onChange(geometry.size[keyPath: keyPath]) }
                    .onChange(of: geometry.size[keyPath: keyPath], perform: onChange)
            }
        }
    }
    
    func readSize<T: Equatable>(
        of keyPath: KeyPath<CGSize, T> = \.self,
        to binding: Binding<T>
    ) -> some View {
        readSize(of: keyPath) { newValue in
            binding.wrappedValue = newValue
        }
    }
}

これを使えば指定したViewの高さと幅を動的に取得することができます。

ContentView.swift
struct ContentView: View {
    @State private var text: String = ""
    @State private var contentWidth: CGFloat = .zero
    var body: some View {
        VStack {
            TextField("", text: $text)
                .textFieldStyle(.roundedBorder)
                .padding()
            
            Text(text)
                .padding()
                .foregroundColor(.white)
                .background(Color.green)
                .cornerRadius(10)
                .readSize(of: \\.width, to: $contentWidth)
            
            RoundedRectangle(cornerRadius: 10, style: .continuous)
                .fill(.blue)
                .frame(width: contentWidth, height: 50)
        }
    }
}

このコードではTextのwidthを取得して、RoundedRectangleのwidthを変化させています。

まとめ

今回はKeyPathについて解説しました。

KeyPathを適切な場面で使用すると、コードをシンプルにすることができます。

ぜひご自身の手で色々と試してみてください。

ここまでのご閲覧ありがとうございました!

参考にした記事

SwiftのKeyPathの使いどころ

コメント

タイトルとURLをコピーしました