SwiftUIにおけるView間のデータ受け渡し方法について


# はじめに
この投稿は[egeniq社の技術ブログ](https://www.egeniq.com/blog/deeper-understanding-swiftui)を私なりに解釈したものになります。
もし誤っている箇所がありましたらコメントでご指摘ください。

# SwiftUIで遭遇するView間のデータ受け渡し方法の疑問

SwiftUIではUI部品は全てViewを継承した構造体であるため、UIKitのUIViewクラスのようにパブリックプロパティや初期化時の引数で値を渡すことができません。UIKitを経験する人間からすると初期の段階で遭遇する疑問だと思います。

結論から言うと、SwiftUIでは親ビューと子ビュー間で値を渡す方法は、下記の3つが挙げられます。

- Environment
- EnvironmentObjects
- Preferences

# Environment

Environmentとは、@1amageekさんの[EnvironmentValuesを制するものはSwiftUIを制する](https://qiita.com/1amageek/items/59c6bb32a6627b4fb712)でも説明の通り、そのViewが持つ環境変数です。独自の環境変数を定義することができ、それを利用して親ビューから任意の値を渡すことが可能です。

### 通常の標準のEnvironmentの利用方法

###### 子ビュー
``` Swift
struct EditableNameView: View {
   @Environment(\.editMode) var mode
   @State var name = ""
   var body: some View {
      TextField("Name", text: $name)
         .disabled(mode?.wrappedValue == .inactive)
   }
}
```

上の例だと、[editMode](https://developer.apple.com/documentation/swiftui/editmode)と言う名前の環境変数をEditableNameViewが受け付けられることを表しています。そして、modeが.inactiveの場合内包するTextFieldを無効化するという処理を行っています。

利用する親ビューでは次のように値を渡します。

###### 親ビュー
``` Swift
struct ContentView: View {
    @State var mode: EditMode = .inactive
    
    var body: some View {
    	EditableNameView().environment(\.editMode, mode)
    }
}
```

尚、このeditModeと言う環境変数は[公式リファレンス](https://developer.apple.com/documentation/swiftui/environmentvalues)によると型がBindingのため、親ビューから渡した変数の値は子ビューで変更されると親もその変更した値を知ることができます


> var editMode: Binding<EditMode>?
The mode indicating whether the user can edit the contents of a view associated with this environment.

### 独自のEnvironmentの利用方法

標準の環境変数だけでは独自の値を渡すことができませんが、EnvironmentKeyというプロトコルを継承することで独自の環境変数を定義することができます。

###### 独自環境変数
``` Swift
struct IntEnvKey: EnvironmentKey {
    static var defaultValue: Int = Int()
}
```

このままでは、キー名を定義しただけで、実際に利用するためにはEnvironmentValuesをextensionで拡張する必要があります。

###### EnvironmentValuesの拡張
``` Swift
extension EnvironmentValues {
    var intEnvKey: Int {
        get { self[IntEnvKey.self] }
        set { self[IntEnvKey.self] = newValue}
    }
}
```

先ほどと同様に子ビューで、IntEnvKeyに対応していることを宣言します。

###### 子ビュー
``` Swift
struct CounterView: View {
   @Environment(\.intEnvKey) var count

   var body: some View {
      Text("count: \(count)")
   }
}
```

親ビューでも先ほどと同様に.environmentで値を設定します。

###### 親ビュー
``` Swift
struct ContentView: View {
    @State var mode: EditMode = .inactive
    
    var body: some View {
    	CounterView().environment(\.intEnvKey, 10)
    }
}
```

説明の都合上Intを指定しましたが、独自のクラスを指定することもできるため子ビューへの値渡し方法として利用ができます。

# EnvironmentObjects

これは公式のチュートリアルでも紹介されている方法で、他の２つに比べて一般的な方法だと思います。利用するためには下記のように独自のクラスを定義する必要があります。
一見すると、値渡しをするためだけにクラスを定義するの？と思ってしまいますが、MVVMのModelとしてクラスを定義してしまえば存在する意義は強くなりますし全体の見通しも良くなると思います。

###### 独自クラス
``` Swift
class Person: ObservableObject {
    @Published var firstName: String = ""
    @Published var lastName: String = ""
    
    var name : String {
        get {
            firstName + " " + lastName
        }
    }
}
```

###### 子ビュー
``` Swift
struct PersonView: View {
    @EnvironmentObject private var person: Person
    
    var body: some View {
        VStack {
            Text(person.name)
        }
    }
}
```

###### 親ビュー
``` Swift
struct ContentView: View {
    @ObservedObject var person = Person()
    
    var body: some View {
        PersonView().environmentObject(person)
    }
}
```

# Preferences

先に紹介した２つの方法は基本的に親から子へ伝達させる方法でしたが、Preferenceは子から親へ伝達させる方法です。

まずはPreferenceKeyを継承して独自のPreferenceを定義します。

###### 独自Preference
``` Swift
struct BoolPreference: PreferenceKey {
    typealias Value = Bool

    static var defaultValue: Value = false
    
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = nextValue() || value
    }
}
```

子ビューでは自身が生成されたタイミングで独自Preferenceの値をtrueに設定します。

###### 子ビュー
``` Swift
struct StateView: View {
    var body: some View {
        VStack {
            Text("Hello")
        }.preference(key: BoolPreference.self, value: true)
    }
}
```

親ビューではonPreferenceChangeを仕掛け、値が変わったタイミングで変わった値を取得するようにします。

###### 親ビュー
``` Swift
struct GameView: View {
    @State var state: Bool
    
    var body: some View {
        StateView()
        .onPreferenceChange(BoolPreference.self) { value in
            self.state = value
        }
    }
}
```

このようにすることで子から親に値を伝達します。

# おわりに

SwiftUIでアプリを組む際には、基本的にはEnvironmentObjectsを使って値を伝達し、必要に応じてEnvironmentとPreferencesを使っていけば良いのかと思います。
それぞれどのような場合に使い分けをするのかについてはまだ知見が固まっておらず、分かったら別途投稿をしたいと思います。