【SwiftUI】プロパティラッパー / PropertyWrapper
プロパティラッパーについて調べたことをもとに、まとめてみました。
この記事は筆者の学習のアウトプットとして書いていますので、ご参考程度にご覧ください。
プロパティラッパーって何?
プロパティラッパーとは、プロパティの値の読み書きに対して特定の処理を自動で適用する仕組みのことを言います。
例えば、ある、メニューの名前を入力すると、頭文字が大文字になって返ってくるといった処理を自動で行うといったことです。
// @propertyWrapperを使ってプロパティラッパーを定義
@propertyWrapper
struct Capitalized {
// 内部で保持する値
private var value: String = ""
// wrappedValueは、ラップされたプロパティの値にアクセスするためのプロパティ
var wrappedValue: String {
get { value } // 値を取得
set { value = newValue.capitalized } // 値を設定するときにキャピタライズ(最初の文字を大文字に)する
}
// 初期化メソッド。初期値を設定する
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
// Recipe構造体を定義し、@Capitalizedプロパティラッパーを使用
struct Recipe {
@Capitalized var ingredient: String // ingredientプロパティにCapitalizedラッパーを適用
}
// Recipeインスタンスを作成し、"apple"を初期値として設定
var recipe = Recipe(ingredient: "apple")
// ingredientプロパティを出力。ラッパーにより、"apple"が"Apple"に変換されている
print(recipe.ingredient) // "Apple"
SwiftUIのプロパティラッパー
では、propertyWrapperがどんなものかを把握した上で、SwiftUIで定義されたpropertyWrapperを見てみましょう!
上記の図は以下の記事を参考にし、作成しました。
図の概要
値かオブジェクトか
値の変更を監視するものが @State / @Binding / @Environment
オブジェクト(ObservableObject)の変更を監視するのが @StateObject / @ObservedObject / @EnvironmentObject
『State / Binding』 と 『StateObject / ObservedObject』
@Stateと@Bindingは同時に使われることが多いですが、これをクラスバージョンにすると、@StateObjectと@ObservedObjectの対応関係になるということ。
State / Binding を使った例
struct ParentView: View {
@State private var counter = 0
var body: some View {
ChildView(counter: $counter)
}
}
struct ChildView: View {
@Binding var counter: Int
var body: some View {
VStack {
Text("Counter: \(counter)")
Button(action: {
counter += 1
}) {
Text("Increment")
}
}
}
}
StateObject / ObservedObject を使った例
// Counterクラスは、@Published属性を持つプロパティを含むObservableObjectプロトコルに準拠したクラスです。
// @Published属性により、値が変更されるとビューが更新されます。
class Counter: ObservableObject {
@Published var value = 0
}
// ParentViewは、@StateObject属性を持つカウンターインスタンスを管理します。
// @StateObjectにより、ParentViewのライフサイクルに応じてCounterオブジェクトが作成および保持されます。
struct ParentView: View {
@StateObject private var counter = Counter()
var body: some View {
// ChildViewにcounterオブジェクトを渡します。
ChildView(counter: counter)
}
}
// ChildViewは、@ObservedObject属性を持つカウンターオブジェクトを受け取ります。
// @ObservedObjectにより、Counterオブジェクトの値の変化を監視し、変化があった場合にビューが再描画されます。
struct ChildView: View {
@ObservedObject var counter: Counter
var body: some View {
VStack {
// Counterオブジェクトのvalueプロパティを表示します。
Text("Counter: \(counter.value)")
// ボタンを押すと、Counterオブジェクトのvalueプロパティがインクリメントされます。
Button(action: {
counter.value += 1
}) {
Text("Increment")
}
}
}
}
何でプロパティラッパーが必要なの?
結論、SwiftUIのViewは構造体だからです。
構造体というのは値の変更ができないという性質を持っています。そこでプロパティラッパーは、構造体において状態を効率的に管理するための仕組みを提供します。
構造体のメリット
ではなぜ構造体にする必要があったのかという疑問が生まれてくるかもしれません。SwiftUIのViewが構造体である理由は、いくつかの重要な利点があるみたいです。
Appleの公式Dドキュメントによると、構造体とクラスで迷ったら、構造体を選ぶことが推奨されています。構造体での値の変更は意図的に明示しない限り行われないため、意図しない副作用を避け、コードの安全性と予測可能性を向上させることができます。
プロパティラッパーをざっくり
プロパティラッパーを「とあるケーキ屋さん」を例に挙げて説明してみます。
@State
@Stateは、Viewが自分自身で管理するローカルな状態を保持します。変更があるとViewが再描画されます。
パティシエAさんは、自分専用の「秘密のレシピノート」 (View) を持っています。このノートにはAさんが直接書き込んでいる材料の詳細が載っており、自由に変更することができます。ある日、Aさんは砂糖の分量(プロパティ)を50グラムから100グラムに変更しました。この変更はAさんだけが見ることができる「秘密のレシピノート」に反映されます。
@Binding
@Bindingは、親Viewから子Viewに状態を渡すために使用されます。子Viewでの変更が親Viewに伝わります。
パティシエAさんとパティシエBさんは、レシピを共有するための「共有レシピノート」を持ちたいと考えました。このノートにはAさんが変更した砂糖の分量(値)が反映されるようになっています。Aさんが砂糖の分量(値)を変更すると、その変更はBさんのノートにもすぐに反映されます。
@StateObject
@StateObjectは、Viewのライフサイクルにわたってオブジェクトの状態を保持します。初めてオブジェクトを作成する場合に使用します。
Aさんは砂糖の分量(値)だけでなく、いちごの分量(値)も変更したくなったようです。Aさんは、それぞれの材料の変更( 砂糖の分量といちごの分量)をリスト(オブジェクト)で管理することにしました。
@ObservedObject
@ObservedObjectは、他のオブジェクトの状態を観察し、その変更に応じてViewを更新します。
パティシエBさんは、Aさんがつくった材料リスト(オブジェクト)の変更がしたいと考えました。そこで、AさんとBさん共有の材料リスト(オブジェクト)を作成しました。このリストはAさんBさん双方向で変更が可能です。
@EnvironmentObject
@EnvironmentObjectは、アプリ全体で共有されるオブジェクトを提供します。多くのViewで同じデータにアクセスするために使用します。
パティシエCさんは、レストラン全体で使用するメニュー管理システムを作りました。このシステムを使うことで、レストランの全員が同じメニュー情報を共有し、変更があった場合に全員にその情報が反映されます。
@Environment
@Environmentは、システムから提供される値や、上位の環境設定を参照するために使用されます。
レストラン「ねこりぼっちカフェ」のオーナーであるパティシエDさんは、レストラン全体の雰囲気を統一するために、テーマカラーを決めることにしました。
参考
Discussion