Property Wrapper:SwiftUIにおける基本概念、構文と例

著者: IT Sectr 公開日: 2026-06-19 読了時間: 8 分

Property Wrapperは、コードを重複させることなくプロパティへのアクセスと変更にロジックの層を追加するSwiftのメカニズムです。SwiftUIでは、Property Wrappersがビューの状態管理の基盤となっています:@State、@Binding、@ObservedObject、@StateObject、@Environment。Swiftドキュメント(2025)によると、プロパティラッパーはプロジェクトのボイラープレートコードを平均40%削減します。Property Wrapperを理解することは、すべてのiOS開発者がフレームワークを効果的に扱うために不可欠です。

重要なポイント

  • Property Wrapper — プロパティのゲッターとセッターにロジックを追加するSwiftの構文構造
  • @State — 単一のビューに属するローカル状態のためのラッパー
  • @Binding — 親ビューからのデータソースとの双方向接続
  • @ObservedObject — 所有権なしで外部のObservableObjectを監視
  • @StateObject — 現在のビューレベルでのObservableObjectの作成と所有

Property Wrapperとは?

Property Wrapper — バージョン5.1で導入されたSwift言語の構造で、プロパティアクセスのロジックを別の型にカプセル化できます。各クラスで繰り返しゲッターやセッターを書く代わりに、開発者はラッパーを一度宣言し、型の前に@アノテーションを付けて適用します。Swiftは自動的にプロパティを指定された型でラップし、読み書き時にwrappedValueとprojectedValueのメソッドを呼び出します。Apple(WWDC 2019)によると、Property WrappersはSwiftUIの主要な抽象化となりました。

プロパティラッパーは、@propertyWrapper属性を持つ構造体またはクラスです。内部では、そのような型はwrappedValueプロパティを実装する必要があり、実際の値を返して設定します。Swiftコンパイラは元のプロパティへのアクセスをwrappedValueの呼び出しに置き換え、呼び出し元のコードから実装を完全に隠蔽します。さらに、$記号でアクセス可能なプロジェクションであるprojectedValueを定義することもできます。

Property Wrappersの利点はロジックの再利用にあります。例えば、メール検証、値のキャッシュ、ストレージ同期のためのラッパーを作成し、プロジェクトの任意のプロパティに適用できます。SwiftUIでは、この概念が至る所で使用されています:各状態管理メカニズムは個別のProperty Wrapperとして実装されています。

SwiftでProperty Wrapperはどのように動作するか?

@WrapperType var value: Tアノテーションでプロパティを宣言すると、Swiftコンパイラはコードを変換します。WrapperTypeのインスタンスを作成し、wrappedValueを通じてプロパティへのアクセスを生成します。ソースコードlet x = valueはlet x = _value.wrappedValueに、value = newValueは_value.wrappedValue = newValueに変換されます。この変換はコンパイル時に行われ、ランタイムオーバーヘッドはありません。

swift
@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を提供し、$observedObjectはPublished.Publisherを返します。プロジェクションにより、ラッパーは値への基本的なアクセスを変更せずに拡張インターフェースを提供できます。

SwiftUIの主要なProperty Wrappers

SwiftUIには状態管理のための5つの組み込みProperty Wrappersがあります:@State、@Binding、@ObservedObject、@StateObject、@Environment。それぞれが特定のタスクを解決し、異なるシナリオで使用されます。@Stateは単純なローカルデータ用、@Bindingは子ビューへのデータ参照の受け渡し用、@ObservedObjectと@StateObjectは複雑なオブジェクト用、@Environmentは階層からのシステム値用です。

ラッパー目的所有権
@State単一ビューのローカル状態現在のビュー
@Binding親との双方向接続親ビュー
@ObservedObject外部オブジェクトの監視外部所有者
@StateObjectObservableObjectの作成現在のビュー
@Environment階層からのシステム値SwiftUI環境

特定のProperty Wrapperの選択は、データソースとそのライフサイクルに依存します。データが単一のビューに属し、子コンポーネントに不要な場合は@Stateを使用します。子ビューが親のデータを変更する必要がある場合は@Bindingを使用します。複数のビューで使用されるオブジェクトには@ObservedObjectと@StateObjectが適しています。

@State:ビューのローカル状態

@Stateは、単一のビュー内にローカル状態を保存するためのProperty Wrapperです。SwiftUIは@Stateプロパティのメモリを自動的に管理し、変更のたびにビューを再描画します。@Stateは、現在のビューにのみ属する単純な型(String、Int、Bool、enum)や構造体に適しています。値が変更されると、SwiftUIはbodyプロパティを再実行します。

swift
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は、別のビューに属するデータソースへの参照を作成します。Bindingはそれ自体では値を保存せず、@State、@StateObject、または親から渡された別のBindingを介してデータを読み書きします。これにより、子コンポーネントはデータを直接所有したりコールバックを使ったりせずに、祖先の状態を変更できます。

swift
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と@StateObject:外部データ

@ObservedObjectは、外部から渡されたObservableObjectインスタンスを監視するためのProperty Wrapperです。ビューはこのオブジェクトを所有しません — 親コンポーネントで作成されるか、Environmentを介して注入されます。ObservableObject内の@Publishedプロパティが変更されると、SwiftUIは@ObservedObjectを介して購読しているすべてのビューを再描画します。

@StateObject — ビュー内で直接ObservableObjectを作成し所有するためのラッパーです。@ObservedObjectとは異なり、@StateObjectはビューのライフサイクル全体でオブジェクトの単一インスタンスを保証します。SwiftUIがビュー構造を再作成しても(これは頻繁に起こります)、@StateObjectは既存のオブジェクトを保持し、イニシャライザを再び呼び出しません。

swift
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は親から既に作成されたオブジェクトを受け取るビューで使用されます。このルールに違反すると、状態の喪失や予期しないデータの再作成が発生します。

カスタムProperty Wrapperの作成

Swiftでは、繰り返し行われるプロパティアクセスロジックのためにカスタムProperty Wrappersを作成できます。@propertyWrapper属性を持つ構造体またはクラスを宣言し、wrappedValueを実装するだけです。以下は、UserDefaultsと自動的に値を同期するUserDefaultsWrapperラッパーです。

swift
@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と@StateObjectの違いは何ですか?

@Stateは単純な型(String、Int、Bool)や構造体向けに設計されており、@StateObjectはObservableObjectを実装する参照型向けです。@Stateは値をSwiftUI内に直接保存し、@StateObjectはヒープ上のクラスインスタンスを管理します。

@Bindingは@Stateなしで使用できますか?

はい、@Bindingは@StateObject、@ObservedObject、または$プロジェクションを使った別のBindingから作成できます。BindingはObservableObjectから$object.$publishedPropertyを介して、またはInlineBindingからBinding.constant(value)を介して初期化することもできます。

アプリケーション全体で使用されるデータにはどのProperty Wrapperを選ぶべきですか?

グローバルデータには、@EnvironmentObjectを使用するか、EnvironmentValuesを介してObservableObjectを注入します。@StateObjectはルートビューに適しており、その後@ObservedObjectを介して子コンポーネントに渡します。

@ObservedObjectがデータを再作成する可能性があるのはなぜですか?

@ObservedObjectはオブジェクトを所有しません — 親ビューが再作成されて新しいインスタンスを渡すと、@ObservedObjectはそれに切り替わります。状態の喪失を防ぐには、所有するビューが@StateObjectを使用する必要があります。

非同期操作用のProperty Wrapperを作成できますか?

はい、ただしクラス内で@Publishedと非同期関数を組み合わせたObservableObjectを使用する方が簡単です。Property Wrapperは本質的に同期的であり、wrappedValueはアクセスのたびに計算されるため、長時間の操作には適していません。

まとめ

  • Property Wrapper — コードの重複なしにプロパティアクセスロジックをカプセル化するためのSwift 5.1の構文構造
  • @State — 単純な型のためのローカル状態、変更時に自動的にビューを再描画
  • @Binding — 所有権なしの親ビューデータへの参照、$プロジェクションでアクセス可能
  • @StateObject — ライフサイクル全体で単一インスタンスを保証するObservableObjectの所有
  • @ObservedObject — 親コンポーネントから渡されたObservableObjectの監視
  • @Environment — SwiftUIシステム環境オブジェクトへのアクセス
  • カスタムラッパーは@propertyWrapper属性とwrappedValueの実装によって作成される

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください