SwiftUIの基本的なデータハンドリング方法


公式ドキュメントを読んでSwiftUIにおけるViewとデータのハンドリングの知識が深まったので、理解の再確認のためにも情報をまとめました。基本的に[公式ドキュメント](https://developer.apple.com/documentation/swiftui/state-and-data-flow)の要約です。

なお、過去に[こちらの記事](https://qiita.com/noby111/items/26405bd89075c841029a)でView間のデータ受け渡し方法の紹介として、EnvironmentやPreferencesを紹介しましたが、基本はState、Binding、ObservableObject(EnvironmentObject)だと考えています。

指摘がありましたら、是非コメントください。

## 基本のStateとBinding

### State

 - Viewの表示内容を制御するための変数
 - publicで宣言した場合は親Viewからコンストラクタ引数として値を受け取ることが可能（初期値は不要）
 - privateで宣言した場合は初期値が必須
 - 自分自身で利用するか子Viewへの値渡しとして利用可能
 - **親Viewから引数で受け取ったStateは子Viewで変更したとしてもその変更は親には伝わらない**

### Binding

 - publicで宣言したStateと同じく、親から値を受け取る
 - **親Viewから受け取ったStateの値変更を親に伝えたいときに利用する**

StateとBindingは近い親子関係や自分自身で完結する場合にのみ使うべきです。
図で書くとこんな感じです。

![スクリーンショット 2020-11-14 23.15.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200963/f76ecd21-1fff-e133-e40a-7a78ba38d209.png "図１")

## 複数Viewを考慮すると必要になるObservableObject(EnvironmentObject)

先ほどのStateとBindingを使えばView間のデータのやり取りは可能です。
しかし、親子関係を持たないView間（図２）や階層の深いViewへのデータ渡し（図３）はどうすれば良いでしょうか？
そのような場合はStateのバケツリレーではなくObservableObjectを使います。

![スクリーンショット 2020-11-15 23.12.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200963/9e5e33c3-0117-8a86-8566-e99382195575.png "図２")

![スクリーンショット 2020-11-15 23.12.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200963/ba3e6422-f8c4-0950-d564-1a6c65101e49.png "図３")


### ObservableObject

``` Swift
class Book: ObservableObject {
    @Published var title = "Great Expectations"
}
```

 - イメージとしてはState変数を複数持てる構造体
 - 利用側がObservedObjectとして定義した場合、Published宣言された変数の変更を検知することができる
 - Stateのようにコンストラクタ引数にObservedObjectを渡すことで子Viewで値の参照&変更をすることができる
 - **子View側でPublished宣言された変数を変更すると、親View側で変更を検知することができる**

### EnvironmentObject

コンストラクタ引数で渡さないと行けないのであれば、先に挙げた課題は解決しないとのではと思った方は鋭いです。
いちいち、バケツリレーするのは面倒ですし、コピーミスが発生する恐れがあります。
そのような場合は EnvironmentObject を使います。親ViewでEnvironmentObjectとして、ObservableObjectインスタンスの指定があれば、そのViewの全ての子孫ViewがObservableObjectの値を参照することができます。
また、図２の画面AとBは直接の親子関係はありませんが、SwiftUIは全ての画面はSceneDelegateで指定されるRootViewの子孫関係にあり、元を辿れば必ず同じViewを見つけることができます。

```Swift
struct BookReader: App {
    @StateObject var library = Library()
    
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environmentObject(library)
        }
    }
}
```

 - 上の例で言うと、LibraryがObservableObject
 - @StateObjectはObservableObjectの大元、Stateと同じく値を保持するため@StateObjectとして宣言する
 - LibraryView以下の子孫にLibraryを参照変更させたい場合はenvironmentObject()でLibraryのインスタンスを指定する

``` Swift
struct LibraryView: View {
    @EnvironmentObject var library: Library
    
    // ...
}
```

 - 利用側はEnvironmentObjectとしてメンバ定義を行う
 - 親のenvironmentObject()で指定されたインスタンスがここに入る
 - library内の@Published宣言された値を変更すると、LibraryをEnvironmentObjectとして保持している親と子孫にその変更が伝わる

### EnvironmentObjectを使った時のイメージ

![スクリーンショット 2020-11-15 23.45.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200963/f2dcd0f9-f50c-0a61-87ac-f4b6f0a4bbc5.png)

![スクリーンショット 2020-11-15 23.45.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200963/16a8a70f-c147-b4b2-db24-dfaf8d82bf81.png)

# まとめ

- 親と子のような関係が浅いView間のデータやり取りはState/Bindingで十分
- 親子関係の無いView間でのデータやり取りはObservableObject(EnvironmentObject)を使う
- 親、孫、ひ孫といった深いView間のデータやり取りはObservableObject(EnvironmentObject)を使う