LoginSignup
12

【SwiftUI】@StateObjectと@ObservedObjectの違いと使い分け

Posted at

違いについて

結論:ライフサイクルが異なる

それぞれのライフサイクルは以下のようになっています。

Property Wrapper ライフサイクル
@StateObject Viewが表示されてから非表示になるまで
@ObservedObject 親Viewのbodyが更新される度

上記の特徴から@ObservedObjectを付与したプロパティにデータオブジェクトを持たせることは基本的にNGで、親Viewから渡されるデータを参照するのみの場合に用いるべきです。
言葉だけだとイメージが難しい為、コードを使って具体例を解説していきます。

@ObservedObjectがうまく機能しない具体例

以下はあるViewの子Viewを二つ用意し、それぞれ@StateObjectと@ObservedObjectを付与したデータオブジェクトを持たせたアプリです。
SwitchView.png

アプリの概要:

  • 親Viewの"Change"ボタンをタップ時、Imageが切り替わる(bodyが更新される)
  • "increment"というボタンをタップする度にcountが+1される

それではアプリのコードを確認していきましょう。

アプリを作成する

1.データオブジェクトの定義

データオブジェクト
final class DataSource: ObservableObject {
    // 監視したい値には@Publishedをつける
    @Published var count = 0
}

まずは今回の例で使用するデータオブジェクトのクラスを定義します。

2.@ObservedObjectを付与したプロパティを持つViewを定義

@observedObject
struct ObservedObjectCountView: View {
    // 本来自身でデータオブジェクトを保持すべきではない。
    @ObservedObject private var dataSource = DataSource()

    var body: some View {
        VStack {
            Text("子View")
            Text("ObservedObject count: \(dataSource.count)")
            Button("increment") {
                dataSource.count += 1
            }
        }
    }
}

続いて、@ObservedObjectを付与したプロパティにデータオブジェクトを持たせたViewを定義します。繰り返しになりますが、本来は@ObservedObjectにデータを持たせるべきではありません。

3.@StateObjectを付与したプロパティを持つViewを定義

@observedObject
struct StateObjectCountView: View {
    @StateObject private var dataSource = DataSource()

    var body: some View {
        VStack {
            Text("子View")
            Text("StateObject count: \(dataSource.count)")
            Button("increment") {
                dataSource.count += 1
            }
        }
    }
}

@ObservedObjectのViewと同様のViewを作成します。

4.二つのViewの親Viewを定義

親View
// ボタンをタップするとサークルのカラーが切り替わるViewを定義
struct SwitchColorView: View {
    // 値型のデータをView自身に保持させるnode@Stateを使用
    @State private var isFire = false

    var body: some View {
        VStack {
            Spacer()
            Text("親View")
            // true = 赤色, false = 無色
            isFire == true ?
            Image(systemName: "flame")
                .font(.system(size: 200))
                .foregroundColor(.none)
            :
            Image(systemName: "flame.fill")
                .font(.system(size: 200))
                .foregroundColor(.red)

            // 処理を実行するとデータに変更が加えられる為,bodyが再描画される
            // bodyが再描画されると@ObservedObjectのライフサイクルが呼ばれる
            Button("Change") {
                // isFireのBool値を反転させる
                isFire.toggle()
            }
            // 🍏作成した二つのViewを追加
            Spacer()
            StateObjectCountView()
            Spacer()
            ObservedObjectCountView()
            Spacer()
        }
    }
}

先ほど定義した二つのViewのインスタンスを生成して子Viewとして追加して完成です。

解説で使用したアプリのソースコード:

完成したアプリの挙動を確認する

※アニメーション早くて分かりづらかったらすみません..!
Videotogif.gif
親ViewのChangeボタンタップ時のそれぞれの子Viewのcountに注目してください。
@StateObject countは値が変化しませんが、ObservedObject countは0に変化してしまっていることが分かると思います。

原因

SwiftUIではデータに変更がある度にbodyが更新されるようになっているそうです。
そして前述の通り、ObservedObjectのライフサイクルは親Viewのbodyが更新されるタイミングです。
つまり、原因は以下の通りだと考えられます。

【ObservedObject countが0に変化した原因】
親ViewのChangeボタンをタップ
⬇️
親ViewのデータであるisDangerに変更処理が加えられたことでbodyが更新される
⬇️
@ObservedObjectのライフサイクルによってデータオブジェクトのインスタンスが破棄されてしまい、初期状態に戻ってしまった

こういった理由で@ObservedObjectは基本的にはデータオブジェクトを保持させず、親Viewから渡されるデータを参照する場合にのみの用いるべきなのかなと解釈しました。

@ObservedObjectの適切な使い方

struct StateObjectCountView: View {
    @StateObject private var dataSource = DataSource()

    var body: some View {
        VStack {
            Text("子View")
            Text("StateObject count: \(dataSource.count)")
            Button("increment") {
                dataSource.count += 1
            }
            // 🍏データオブジェクトを渡す
            ObservedObjectCountView(dataSource: dataSource)
        }
    }
}

struct ObservedObjectCountView: View {
    // 🍏データオブジェクトを自身に保持させず、親Viewなど外部から受け取るようにする
    @ObservedObject var dataSource: DataSource

    var body: some View {
        VStack {
            Text("子View")
            Text("ObservedObject count: \(dataSource.count)")
            Button("increment") {
                dataSource.count += 1
            }
        }
    }
}

上記のコードのように、@ObservedObjectにはデータオブジェクトを保持させず、親Viewから渡されるデータを参照するようにすると良いと考えます。

まとめると以下のような感じでしょうか。

Property Wrapper 望ましい使用条件
@StateObject ①参照型のデータを扱う
②データの発生源はView自身
@ObservedObject ①参照型のデータを扱う
②データの発生源は親Viewなど外部

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12