Property Wrapperは、コードを重複させることなくプロパティへのアクセスと変更にロジックの層を追加するSwiftのメカニズムです。SwiftUIでは、Property Wrappersがビューの状態管理の基盤となっています:@State、@Binding、@ObservedObject、@StateObject、@Environment。Swiftドキュメント(2025)によると、プロパティラッパーはプロジェクトのボイラープレートコードを平均40%削減します。Property Wrapperを理解することは、すべてのiOS開発者がフレームワークを効果的に扱うために不可欠です。
重要なポイント
Property Wrapper — バージョン5.1で導入されたSwift言語の構造で、プロパティアクセスのロジックを別の型にカプセル化できます。各クラスで繰り返しゲッターやセッターを書く代わりに、開発者はラッパーを一度宣言し、型の前に@アノテーションを付けて適用します。Swiftは自動的にプロパティを指定された型でラップし、読み書き時にwrappedValueとprojectedValueのメソッドを呼び出します。Apple(WWDC 2019)によると、Property WrappersはSwiftUIの主要な抽象化となりました。
プロパティラッパーは、@propertyWrapper属性を持つ構造体またはクラスです。内部では、そのような型はwrappedValueプロパティを実装する必要があり、実際の値を返して設定します。Swiftコンパイラは元のプロパティへのアクセスをwrappedValueの呼び出しに置き換え、呼び出し元のコードから実装を完全に隠蔽します。さらに、$記号でアクセス可能なプロジェクションであるprojectedValueを定義することもできます。
Property Wrappersの利点はロジックの再利用にあります。例えば、メール検証、値のキャッシュ、ストレージ同期のためのラッパーを作成し、プロジェクトの任意のプロパティに適用できます。SwiftUIでは、この概念が至る所で使用されています:各状態管理メカニズムは個別のProperty Wrapperとして実装されています。
@WrapperType var value: Tアノテーションでプロパティを宣言すると、Swiftコンパイラはコードを変換します。WrapperTypeのインスタンスを作成し、wrappedValueを通じてプロパティへのアクセスを生成します。ソースコードlet x = valueはlet x = _value.wrappedValueに、value = newValueは_value.wrappedValue = newValueに変換されます。この変換はコンパイル時に行われ、ランタイムオーバーヘッドはありません。
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
リストにはCapitalizedラッパーが示されており、文字列を自動的に大文字形式に変換します。値を代入すると、セッターは保存前にcapitalizedを呼び出します。これで@Capitalizedアノテーションを持つプロパティは、正しくフォーマットされたテキストのみを保存します。このアプローチにより、検証とフォーマットのコードの重複が完全に排除されます。
プロジェクション(projectedValue) — $プレフィックスを介してアクセス可能な追加の通信チャネルです。SwiftUIでは、この機能が至る所で使用されています:$stateはBinding
SwiftUIには状態管理のための5つの組み込みProperty Wrappersがあります:@State、@Binding、@ObservedObject、@StateObject、@Environment。それぞれが特定のタスクを解決し、異なるシナリオで使用されます。@Stateは単純なローカルデータ用、@Bindingは子ビューへのデータ参照の受け渡し用、@ObservedObjectと@StateObjectは複雑なオブジェクト用、@Environmentは階層からのシステム値用です。
| ラッパー | 目的 | 所有権 |
|---|---|---|
| @State | 単一ビューのローカル状態 | 現在のビュー |
| @Binding | 親との双方向接続 | 親ビュー |
| @ObservedObject | 外部オブジェクトの監視 | 外部所有者 |
| @StateObject | ObservableObjectの作成 | 現在のビュー |
| @Environment | 階層からのシステム値 | SwiftUI環境 |
特定のProperty Wrapperの選択は、データソースとそのライフサイクルに依存します。データが単一のビューに属し、子コンポーネントに不要な場合は@Stateを使用します。子ビューが親のデータを変更する必要がある場合は@Bindingを使用します。複数のビューで使用されるオブジェクトには@ObservedObjectと@StateObjectが適しています。
@Stateは、単一のビュー内にローカル状態を保存するためのProperty Wrapperです。SwiftUIは@Stateプロパティのメモリを自動的に管理し、変更のたびにビューを再描画します。@Stateは、現在のビューにのみ属する単純な型(String、Int、Bool、enum)や構造体に適しています。値が変更されると、SwiftUIはbodyプロパティを再実行します。
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("カウント: \(count)")
Button("インクリメント") {
count += 1
}
}
}
}
この例では、@Stateプロパティcountがカウンターの現在の値を保存しています。SwiftUIはこのプロパティのためにヒープ上にストレージ領域を作成し、CounterViewのライフサイクルに結び付けます。ボタンを押すとcountが1増加し、SwiftUIが変更を検出してbodyを再実行し、新しい値を表示します。重要:@Stateは複雑な参照型には使用すべきではありません — そのためには@StateObjectと@ObservedObjectがあります。
@Bindingは、別のビューに属するデータソースへの参照を作成します。Bindingはそれ自体では値を保存せず、@State、@StateObject、または親から渡された別のBindingを介してデータを読み書きします。これにより、子コンポーネントはデータを直接所有したりコールバックを使ったりせずに、祖先の状態を変更できます。
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
リストでは、ToggleSwitchが親ビューから@BindingBoolを受け取っています。親は@State var isToggleOn = falseを作成し、$isToggleOnをToggleSwitchのイニシャライザに渡します。ユーザーが子ビュー内でトグルスイッチを切り替えると、変更は即座に親の@Stateに反映されます。Bindingメカニズムにより、階層を上方向に変更を伝達するためのデリゲートやクロージャが完全に不要になります。
@ObservedObjectは、外部から渡されたObservableObjectインスタンスを監視するためのProperty Wrapperです。ビューはこのオブジェクトを所有しません — 親コンポーネントで作成されるか、Environmentを介して注入されます。ObservableObject内の@Publishedプロパティが変更されると、SwiftUIは@ObservedObjectを介して購読しているすべてのビューを再描画します。
@StateObject — ビュー内で直接ObservableObjectを作成し所有するためのラッパーです。@ObservedObjectとは異なり、@StateObjectはビューのライフサイクル全体でオブジェクトの単一インスタンスを保証します。SwiftUIがビュー構造を再作成しても(これは頻繁に起こります)、@StateObjectは既存のオブジェクトを保持し、イニシャライザを再び呼び出しません。
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
}
struct ProfileView: View {
@StateObject var settings = UserSettings()
var body: some View {
ChildProfileView(settings: settings)
}
}
struct ChildProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
Text("こんにちは、\(settings.username)")
}
}
この例では、ProfileViewが@StateObjectを介してUserSettingsを作成し、オブジェクトの所有者になります。ChildProfileViewは@ObservedObjectを介して同じインスタンスを受け取り、監視はしますがライフサイクルは管理しません。usernameが変更されると、両方のビューが更新されます。ChildProfileViewが@ObservedObjectの代わりに@StateObjectを使用した場合、レンダリングのたびに初期値を持つ新しいインスタンスが作成されます。
重要なルール:@StateObjectはオブジェクトを作成するビュー(真実のソース)で使用され、@ObservedObjectは親から既に作成されたオブジェクトを受け取るビューで使用されます。このルールに違反すると、状態の喪失や予期しないデータの再作成が発生します。
Swiftでは、繰り返し行われるプロパティアクセスロジックのためにカスタムProperty Wrappersを作成できます。@propertyWrapper属性を持つ構造体またはクラスを宣言し、wrappedValueを実装するだけです。以下は、UserDefaultsと自動的に値を同期するUserDefaultsWrapperラッパーです。
@propertyWrapper
struct UserDefaultsWrapper<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
struct AppConfig {
@UserDefaultsWrapper(key: "theme", defaultValue: "light")
var theme: String
}
UserDefaultsWrapperラッパーは、UserDefaultsがサポートする任意のデータ型で動作するようにジェネリックTを使用しています。ゲッターはキーで値を読み取り、セッターは書き込みます。themeプロパティに@UserDefaultsWrapper(key:defaultValue:)を適用すると、自動的にストレージにバインドされます — すべてのUserDefaultsロジックはラッパー内に隠蔽されています。これはProperty Wrappersによるボイラープレートコード削減の典型的な例です。
カスタムラッパーを作成する際は、パフォーマンスを考慮することが重要です。ゲッターとセッターはプロパティにアクセスするたびに呼び出されるため、wrappedValueに重いI/O操作を置くべきではありません。非同期データストレージの場合は、Property WrappersをObservableObjectや@Publishedと組み合わせるのが良いでしょう。
よくある質問
@Stateは単純な型(String、Int、Bool)や構造体向けに設計されており、@StateObjectはObservableObjectを実装する参照型向けです。@Stateは値をSwiftUI内に直接保存し、@StateObjectはヒープ上のクラスインスタンスを管理します。
はい、@Bindingは@StateObject、@ObservedObject、または$プロジェクションを使った別のBindingから作成できます。BindingはObservableObjectから$object.$publishedPropertyを介して、またはInlineBindingからBinding.constant(value)を介して初期化することもできます。
グローバルデータには、@EnvironmentObjectを使用するか、EnvironmentValuesを介してObservableObjectを注入します。@StateObjectはルートビューに適しており、その後@ObservedObjectを介して子コンポーネントに渡します。
@ObservedObjectはオブジェクトを所有しません — 親ビューが再作成されて新しいインスタンスを渡すと、@ObservedObjectはそれに切り替わります。状態の喪失を防ぐには、所有するビューが@StateObjectを使用する必要があります。
はい、ただしクラス内で@Publishedと非同期関数を組み合わせたObservableObjectを使用する方が簡単です。Property Wrapperは本質的に同期的であり、wrappedValueはアクセスのたびに計算されるため、長時間の操作には適していません。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。