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

# 違いについて
#### 結論：ライフサイクルが異なる

それぞれのライフサイクルは以下のようになっています。
| Property Wrapper | ライフサイクル |
|:-----------|:------------|
| <span>@</span>StateObject | Viewが表示されてから非表示になるまで | 
| <span>@</span>ObservedObject | 親Viewのbodyが更新される度 | 

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

# <span>@</span>ObservedObjectがうまく機能しない具体例

以下はあるViewの子Viewを二つ用意し、それぞれ<span>@</span>StateObjectと<span>@</span>ObservedObjectを付与したデータオブジェクトを持たせたアプリです。
![SwitchView.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877854/b46b8115-d7fa-30fc-f6c8-0f7971340e58.png)
### アプリの概要：
- 親Viewの"Change"ボタンをタップ時、Imageが切り替わる(bodyが更新される)
- "increment"というボタンをタップする度にcountが+1される

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

## アプリを作成する
### 1.データオブジェクトの定義
```swift:データオブジェクト
final class DataSource: ObservableObject {
    // 監視したい値には@Publishedをつける
    @Published var count = 0
}
```
まずは今回の例で使用するデータオブジェクトのクラスを定義します。
### 2.<span>@</span>ObservedObjectを付与したプロパティを持つViewを定義
```swift: @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
            }
        }
    }
}
```
続いて、<span>@</span>ObservedObjectを付与したプロパティにデータオブジェクトを持たせたViewを定義します。繰り返しになりますが、本来は<span>@</span>ObservedObjectにデータを持たせるべきではありません。

### 3.<span>@</span>StateObjectを付与したプロパティを持つViewを定義
```swift: @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
            }
        }
    }
}
```
<span>@</span>ObservedObjectのViewと同様のViewを作成します。

### 4.二つのViewの親Viewを定義
```swift: 親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として追加して完成です。

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

https://github.com/0102JohnnyTd/PracticePropertyWrapper

## 完成したアプリの挙動を確認する
※アニメーション早くて分かりづらかったらすみません..!
![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877854/72c0ccc4-b1ce-34d6-bfe3-0bad5507879e.gif)
親ViewのChangeボタンタップ時のそれぞれの子Viewのcountに注目してください。
**<span>@</span>StateObject countは値が変化しませんが、ObservedObject countは0に変化してしまっている**ことが分かると思います。

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

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

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

# <span>@</span>ObservedObjectの適切な使い方
```swift:
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
            }
        }
    }
}
```
上記のコードのように、<span>@</span>ObservedObjectにはデータオブジェクトを保持させず、親Viewから渡されるデータを参照するようにすると良いと考えます。

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

| Property Wrapper | 望ましい使用条件 |
|:-----------|:------------|
| <span>@</span>StateObject | ①参照型のデータを扱う<br>②データの発生源はView自身 | 
| <span>@</span>ObservedObject | ①参照型のデータを扱う<br>②データの発生源は親Viewなど外部 | 
# 参考
https://blog.personal-factory.com/2021/01/23/how-to-use-propertywrapper-in-swiftui/

https://note.com/taatn0te/n/n1fc0518a8ee6