SwiftUI における「@State, @StateObject」などを使用した状態管理について…

朝の実(Asanomi)アイコン アプリ開発

@State, @Binding, @StateObject, @ObservedObject, ObservableObject, @Published, @EnvironmentObject

SwiftUI で開発を行っている人であれば、
これらのキーワードを一度は目にしたことがあるのではないでしょうか。
(「@」で始まるものは「プロパティラッパー」、「ObservableObject」はプロトコルです。)

画面内で値を保持したり、
画面間で値を共有したり、
値の変更に応じて画面を再描画したりする中で、
何となく使ってきた方も多いのではないでしょうか。

一方で、「どのキーワードを、どの画面で、なぜ使うのか」
を説明しようとすると、意外と迷うことがあります。

これらは、
「値がどこに存在し、画面(View)がそれをどう扱うか」
を明示するための仕組みです。

本記事では、SwiftUI における値の所在と画面との関係を整理しながら、
各キーワードの役割を確認していきます。


※ 本記事では、View と ViewModel を分離する構成(MVVMなど)を前提に説明します。
※「@EnvironmentObject」については View や ViewModel だけで完結する話ではないので本記事では扱わず、別記事で紹介しようと思います。

全体像

まずはキーワードがどこで記述されて、何に付与されるかを、階層構造で整理します。

Viewファイルに記述
├─ Viewファイルが宣言した値に付与
│ ├─ @State(親Viewで使用)
│ └─ @Binding(子Viewで使用)

└─ Viewファイルが宣言したインスタンス(ObservableObject に準拠)に付与
├─ @StateObject(親Viewで使用)
└─ @ObservedObject(子Viewで使用)

ViewModelファイルに記述
└─ ObservableObject に準拠したViewModelファイルが宣言した値に付与
└─ @Published

そして、図で整理します。

SwiftUIの状態管理図(@State, @Binding, ObservableObject, @Published, @StateObject, @ObservedObject)

@State / @Binding / @StateObject / @ObservedObject はすべて View 内の監視対象に使用しますが、「その View が親なのか子なのか」「監視対象が値なのかインスタンスなのか」でどれを使用するかが変わります。
ObservableObject は View が監視したいクラス(ViewModel)に準拠させるプロトコルです。
@Published は 上記ObservableObjectに準拠したクラスの監視対象にしたい値に使用します。(@Publishedのついた値の変更 → そのクラスの変更通知 となります。)

@State と @Binding(View内で完結するやつ)

@State@Binding は、View(画面)内で完結する「値」を扱うための仕組みです。

@State

@State は、その View 自身が所有する値を定義するために使用します。
ボタンの ON / OFF、アラート表示フラグ、入力中のテキストなど、
「この画面の中だけで意味を持つ状態」を表すのに適しています。
@State の値が変更されると、SwiftUI はその変更を検知し、画面(View)を再描画します。

struct ParentView: View {
    @State var status: Bool = false // ← 変更を検知したい値に @State 付与。
    var body: some View {
        Toggle("ON / OFF", isOn: $status)
    }
}
補足

コード内で使用されている「Toggle」 は ON / OFF を切り替えるための SwiftUI の標準コンポーネントで、
第二引数の isOn に渡した値が切り替え操作に応じて更新されます。
以降のコード例でも「Toggle」を多用します。


@Binding

@Binding は 親 View が所有している @State の値を、子 View から参照・更新するための仕組みです。
子 View は親の所有している値を、借りて使うという位置づけです。
もちろん @Binding の値が更新されると、親 View の @State が更新されたことになるので、画面が再描画されます。

struct ChildView: View {
    @Binding var status: Bool // ← 親から借り受ける「@Stateの値」を格納する変数に @Binding を付与
    var body: some View {
        Toggle("ON / OFF", isOn: $status)
    }
}

要点をまとめると
• 親 View:@State で値を所有する
• 子 View:@Binding で親の値を借り受ける

@State@Binding を合わせた例を以下に示します。
親 View 内で子 View を呼び出す際に、引数で@State の値を渡しています。

// 親 View
struct ParentView: View {
    @State var status: Bool = false // ← 変更を検知したい値に @State 付与。
    var body: some View {
        VStack {
            ChildView(childStatus: $status) // ← 子 View の呼び出し時に @State の値を渡す
        }
    }
}
// 子 View
struct ChildView: View {
    @Binding var childStatus: Bool // ← 親から借り受ける「@Stateの値」を格納する変数に @Binding を付与
    var body: some View {
        Toggle("ON / OFF", isOn: $childStatus)
    }
}

@StateObject と @ObservedObject(View外も関係するやつ)

@StateObject@ObservedObject は、View 内で View の外にある「オブジェクト」を扱うための仕組みです。

⚠️重要⚠️

@StateObject ( @ObservedObject ) を付与するオブジェクトは後で解説する
ObservableObjectに準拠している必要があります。

@StateObject

@StateObject は、その View 自身が所有するオブジェクト(主に ViewModel)に使用します。
また、@StateObject が付与されたオブジェクト内の監視対象の値が変更されると、SwiftUI はその変更を検知し、画面(View)を再描画します。

// 親 View
struct ParentView: View {
    @StateObject var viewModel = ToggleViewModel() // ← 変更を検知したいオブジェクト(ViewModel)に @StateObject を付与
    var body: some View {
        Toggle("ON / OFF", isOn: $viewModel.outerStatus)
    }
}

@ObservedObject

@ObservedObject は 親 View が所有している @StateObject のオブジェクト(主に ViewModel)を、子 View から参照・更新するための仕組みです。
子 View は親の所有しているオブジェクトを、借りて使うという位置づけです。
もちろん @ObservedObject が付与されたオブジェクト内の監視対象の値が更新されると、親Viewの @StateObject が更新されたことになるので、画面が再描画されます。

// 子 View
struct ChildView: View {
    @ObservedObject var viewModel: ToggleViewModel // ← 親から借り受ける「@StateObject のオブジェクト(ViewModel)」を格納する変数に @ObservedObject を付与
    var body: some View {
        Toggle("ON / OFF", isOn: $viewModel.outerStatus)
    }
}

要点をまとめると
• 親 View:@StateObject でオブジェクトを所有する
• 子 View:@ObservedObject で親のオブジェクトを借り受ける

@StateObject@ObservedObject を合わせた例を以下に示します。
親 View 内で子 View を呼び出す際に、引数で @StateObject のオブジェクトを渡しています。

struct ParentView: View {
    @StateObject var parentViewModel = ToggleViewModel() // ← ViewModel を所有する
    var body: some View {
        VStack {
            Toggle("ON / OFF", isOn: $parentViewModel.outerStatus)
            ChildView(childViewModel: parentViewModel) // ← 子 View の呼び出し時に ViewModel を渡す
        }
    }
}
struct ChildView: View {
    @ObservedObject var childViewModel: ToggleViewModel // ← 親から借り受ける「ViewModel」を格納する変数に @ObservedObject を付与
    var body: some View {
        Toggle("ON / OFF", isOn: $childViewModel.outerStatus)
    }
}

ちなみに、上記の例で使用しているオブジェクト(ObservableObjectに準拠)は以下です。
ObservableObject@Published については次章で詳しく解説しています。

class ToggleViewModel: ObservableObject {
    @Published var outerStatus: Bool = false
}

ObservableObject と @Published(ViewModel)

ObservableObject@Publishedは、オブジェクト(主に ViewModel)を View から監視できるようにする仕組みです。

ObservableObject

ObservableObject は、「このクラスは View から監視される対象である」ということを SwiftUI に伝えるためのプロトコルです。通常は ViewModel に準拠させて使用します。

class ToggleViewModel: ObservableObject {
}

ただしこの時点では、「このクラスが監視対象になり得る」ことを宣言しただけで、まだ画面更新は発生しません。
次で説明する@Publishedが必要です。


@Published

@Published は、ObservableObject に準拠したクラスの中で「変更を検知したい値」 に付与します。
@Published は View で記述するものではなく、
ObservableObject に準拠したオブジェクトの内部で記述します。

class ToggleViewModel: ObservableObject {
    @Published var status: Bool = false
}

実際の使用例(朝の実)

ここでは、私が開発している朝専用SNSアプリ「朝の実(Asanomi)」で実際に使用しているソースコードを元に使用例を示します。
本アプリでは、画面表示と状態・処理を分離するために、MVVM(Model / View / ViewModel)構成を採用しています。

※投稿処理の詳細は省略します。

ViewModel(PostViewModel.swift)

ViewModel は ObservableObject に準拠し、変更を検知したい値に @Published を付与します。
これにより、@Published を付与した値が更新されたタイミングで SwiftUI が画面を再描画します。

// PostViewModel.swift
import Foundation
@MainActor
final class PostViewModel: ObservableObject {
    private let repository: PostRepository
    // 変更を検知して画面(View)を更新したい値
    @Published var text: String = ""
    @Published var isPosting: Bool = false
    @Published var errorMessage: String?
    init(repository: PostRepository = MockPostRepository()) {
        self.repository = repository
    }
    func post() async {
        // ※朝の実では、入力チェック・投稿実行・エラー処理などをここに実装
        isPosting = true
        defer { isPosting = false }
        do {
            try await repository.createPost(text: text)
            text = ""
            errorMessage = nil
        } catch {
            errorMessage = "投稿に失敗しました"
        }
    }
}
// ※朝の実では以下は別ファイルに分割しています(記事内で完結させるため最小限を記載)
protocol PostRepository {
    func createPost(text: String) async throws
}
struct MockPostRepository: PostRepository {
    func createPost(text: String) async throws {
        // ダミー実装(成功させるだけ)
    }
}

親View(PostView.swift)

親Viewでは、ViewModel(オブジェクト)を @StateObject で「所有」します。
また、画面内だけで完結する UI 制御(アラート表示など)は @State で「所有」します。

// PostView.swift
import SwiftUI
struct PostView: View {
    // 親Viewが ViewModel(オブジェクト)を所有する
    @StateObject private var viewModel = PostViewModel()
    // 親View内だけで完結する状態(値)
    @State private var isShowingErrorAlert: Bool = false
    var body: some View {
        PostComposerView(
            viewModel: viewModel,                        // 子View の @ObservedObject にオブジェクトを渡す
            isShowingErrorAlert: $isShowingErrorAlert     // 子View の @Binding に値を渡す
        )
        .padding()
        .alert("エラー", isPresented: $isShowingErrorAlert) {
            Button("OK") { viewModel.errorMessage = nil }
        } message: {
            Text(viewModel.errorMessage ?? "")
        }
    }
}

子View(PostComposerView.swift)

子Viewでは、親が所有している ViewModel を @ObservedObject で借り受けます。
また、アラート表示などの “画面側の状態” は @Binding で借り受けます。

// PostComposerView.swift
import SwiftUI
struct PostComposerView: View {
    // 親が所有する ViewModel を監視するだけ(子Viewは所有しない)
    @ObservedObject var viewModel: PostViewModel
    // 親Viewが持つ「画面用の状態」を Binding で受け取る
    @Binding var isShowingErrorAlert: Bool
    var body: some View {
        VStack(spacing: 12) {
            Text("朝の実 - 投稿")
                .font(.headline)
            TextEditor(text: $viewModel.text)
                .frame(minHeight: 120)
            Button {
                Task { await viewModel.post() }
            } label: {
                if viewModel.isPosting {
                    ProgressView()
                } else {
                    Text("投稿")
                }
            }
            .disabled(viewModel.isPosting)
        }
        // 子View側で「エラーが出たらアラートを出す」というUI判断だけ行う
        .onChange(of: viewModel.errorMessage) { _, newValue in
            isShowingErrorAlert = (newValue != nil)
        }
    }
}

まとめ

@State / @Binding は、View 内で完結する「値」を扱うための仕組みであり、
@StateObject / @ObservedObject は、View の外にある ViewModel などの「オブジェクト」を扱うための仕組みです。

また、ViewModel 側では ObservableObject に準拠し、@Published を付与した値を変更すると、View 側がその変更を検知し画面が再描画されます。

重要なのは、それぞれのプロパティラッパーを個別に暗記することではなく、
「この状態(値)は View と ViewModel のどこに置くべきか」「親 View が所有するのか、子 View が借り受けるのか」という観点で使い分けることです。

本記事の内容が、SwiftUI における状態管理の全体像を整理する一助になれば幸いです。

sho shimizu

SwiftUI を用いたiOS アプリの個人開発を行っています。
これまでに2本のアプリを App Store にリリースしました。

現在は、朝専用SNS「朝の実(Asanomi)」を開発中です。
CloudKitやSwift Concurrencyを用いた実装を進めながら、
設計や実装上で整理した内容を技術記事にまとめています。

sho shimizuをフォローする
アプリ開発技術解説朝の実
シェアする
sho shimizuをフォローする