SwiftUI @StateObject と @ObservedObject の違い

@State は値型の管理に適しているが、ネットワーク通信やデータベースアクセスなど複雑なロジックを含むモデルはクラスで設計することが多い。こうした参照型のオブジェクトを SwiftUI で扱うために用意されたのが @StateObject@ObservedObject だ。両者は似ているようで、ライフサイクルの管理方法がまったく異なる。

ObservableObject の前提

どちらのプロパティラッパーも、対象クラスが ObservableObject プロトコルに準拠していることが前提となる。@Published を付けたプロパティが変更されると、SwiftUI に通知が送られてビューが再描画される。

class CounterModel: ObservableObject {
    @Published var count = 0

    func increment() {
        count += 1
    }
}

このクラスは count が変わるたびに変更通知を発行する。ビュー側では @StateObject@ObservedObject のどちらかでこのインスタンスを保持し、変更を監視する。

@StateObject:自分で生成し、自分で所有する

@StateObject はビューがオブジェクトを生成して所有する場合に使う。ビューが再描画されてもインスタンスは破棄されず、同じオブジェクトが維持される。@State の参照型バージョンと考えるとわかりやすい。

struct CounterScreen: View {
    @StateObject private var model = CounterModel()

    var body: some View {
        VStack {
            Text("カウント: \(model.count)")
            Button("増やす") {
                model.increment()
            }
        }
    }
}

CounterScreen が何度再描画されても model は同一のインスタンスを指し続ける。初回のビュー生成時にだけインスタンスが作られ、ビューが画面から消えるまで生き続ける。

@ObservedObject:外部から受け取る

@ObservedObject はオブジェクトを外部から受け取って監視する場合に使う。インスタンスの生成責任は呼び出し元にあり、ビュー自体はオブジェクトのライフサイクルを管理しない。

struct CounterDisplay: View {
    @ObservedObject var model: CounterModel

    var body: some View {
        Text("カウント: \(model.count)")
    }
}

このビューは model を所有していないため、親ビューが再描画されるたびにインスタンスが再生成される可能性がある。親から渡されたオブジェクトをそのまま監視するだけ、という位置づけだ。

決定的な違い:再描画時の挙動

両者の最大の違いは、親ビューが再描画されたときの振る舞いにある。

@StateObject

ビューが再描画されてもインスタンスは維持される。初回のみ生成され、ビューの破棄時まで同一のオブジェクトが使われる。

@ObservedObject

ビューが再描画されるとインスタンスが再生成される可能性がある。状態がリセットされるバグの原因になりやすい。

この違いが具体的にどう影響するかを見てみよう。

struct ParentView: View {
    @State private var refresh = false

    var body: some View {
        VStack {
            // 危険: 再描画のたびに新しい CounterModel が作られる
            ChildWithObserved(model: CounterModel())

            Button("親を再描画") {
                refresh.toggle()
            }
        }
    }
}

struct ChildWithObserved: View {
    @ObservedObject var model: CounterModel

    var body: some View {
        Button("カウント: \(model.count)") {
            model.increment()
        }
    }
}

この例では「親を再描画」ボタンを押すたびに CounterModel() が新しく生成され、カウントが 0 に戻ってしまう。@StateObject を使っていればこの問題は起きない。

使い分けの原則

判断基準は単純で、「そのビューがオブジェクトを作るかどうか」で決まる。

@StateObject を使う場面

ビューの中で = Model() のようにインスタンスを生成する場合。そのビューがオブジェクトの「オーナー」として責任を持つ。

@ObservedObject を使う場面

親ビューや外部からインスタンスを受け取る場合。イニシャライザの引数として渡されるケースが典型的。

迷ったら @StateObject を使うのが安全だ。@ObservedObject を誤って使うとインスタンスが予期せず再生成されるバグが発生するが、@StateObject を使いすぎても実害は少ない。

組み合わせのパターン

実際のアプリでは、ルートビューが @StateObject でモデルを生成し、子ビューに @ObservedObject として渡すパターンが一般的だ。

class AppModel: ObservableObject {
    @Published var userName = ""
    @Published var score = 0
}

struct RootView: View {
    @StateObject private var appModel = AppModel()

    var body: some View {
        NavigationStack {
            HomeView(model: appModel)
        }
    }
}

struct HomeView: View {
    @ObservedObject var model: AppModel

    var body: some View {
        VStack {
            Text("ようこそ、\(model.userName)さん")
            Text("スコア: \(model.score)")
        }
    }
}

RootView がオーナーとして AppModel を保持し、HomeView は渡されたインスタンスを監視するだけという関係が明確になっている。

iOS 17 以降の変化

iOS 17 で導入された @Observable マクロを使うと、ObservableObject プロトコルや @Published の記述が不要になり、@StateObject@ObservedObject の区別も @State に一本化される。ただし iOS 16 以前をサポートする必要があるプロジェクトでは、依然として @StateObject / @ObservedObject の理解が不可欠だ。