こんにちは。新人プログラマーの岩本です。
今回は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
日本語に訳すと、こんな感じです。
特定のルート型から特定の結果の値型へのキーパス
すごく簡単に言うと「データ型に定義されたプロパティまでの道のり」です。
定義は以下のようにされています。
class KeyPath<Root, Value>
詳しくは後で解説しますが、Rootが対象のデータ型で、Valueが指定したいプロパティの型です。
ではここから簡単な使用例を解説します。
基本の使用例
以下のような構造体を用意します。
struct Person {
let name: String
let age: Int
}
let person = Person(name: "Ryuto", age: 20)
ここでPersonのnameまでの道のりを持ったKeyPathを宣言します。
let keyPath: KeyPath<Person, String> = \.name
このKeyPathを使えば、以下のようにnameプロパティを参照することができます。
print(person[keyPath: keyPath]) // "Ryuto"
KeyPathを使えばオプショナル型のアンラップも以下のように書くことができます。
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を使わないとこんな書き方になります。
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つにまとめることができます。
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を使った応用例を紹介します。
Viewの拡張関数として、幅と高さを取得するreadSizeを定義します。
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の高さと幅を動的に取得することができます。
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を変化させています。
widthはKeyPathを使用して指定しています。
まとめ
今回はKeyPathについて解説しました。
KeyPathを適切に使用すると、コードがシンプルになると感じました。
ぜひご自身の手で色々と試してみてください。
ここまでのご閲覧ありがとうございました!
参考にした記事
👆実用的な実装例で使い所が理解できました。
👆基本から応用まですごくわかりやすかったです。
コメント