違いについて
結論:ライフサイクルが異なる
それぞれのライフサイクルは以下のようになっています。
Property Wrapper | ライフサイクル |
---|---|
@StateObject | Viewが表示されてから非表示になるまで |
@ObservedObject | 親Viewのbodyが更新される度 |
上記の特徴から@ObservedObjectを付与したプロパティにデータオブジェクトを持たせることは基本的にNGで、親Viewから渡されるデータを参照するのみの場合に用いるべきです。
言葉だけだとイメージが難しい為、コードを使って具体例を解説していきます。
@ObservedObjectがうまく機能しない具体例
以下はあるViewの子Viewを二つ用意し、それぞれ@StateObjectと@ObservedObjectを付与したデータオブジェクトを持たせたアプリです。
アプリの概要:
- 親Viewの"Change"ボタンをタップ時、Imageが切り替わる(bodyが更新される)
- "increment"というボタンをタップする度にcountが+1される
それではアプリのコードを確認していきましょう。
アプリを作成する
1.データオブジェクトの定義
final class DataSource: ObservableObject {
// 監視したい値には@Publishedをつける
@Published var count = 0
}
まずは今回の例で使用するデータオブジェクトのクラスを定義します。
2.@ObservedObjectを付与したプロパティを持つViewを定義
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を定義
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を定義
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として追加して完成です。
解説で使用したアプリのソースコード:
完成したアプリの挙動を確認する
※アニメーション早くて分かりづらかったらすみません..!
親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など外部 |
参考