SwiftUIで状態管理したい時のチートシート

状態管理でよく使われるアノテーションは以下の通りです。
- `@State`
- `@Binding`
- `@StateObject`
- `@ObservedObject`
- `@EnvironmentObject`

# Viewに状態を持たせたい
`@State`をつけると、状態としてViewから変更可能になります。
変更されると即時Viewに反映されます。
```swift
struct ContentView: View {
    @State var count = 0

    var body: some View {
        VStack {
            Text("Count is \(count)")
            Button("Count up") {
                count += 1
            }
        }
    }
}
```

# 子Viewに状態を渡したい
子Viewに渡すだけであれば、単純な引数として渡せばOKです。
親のStateが変更されると、子にそのまま伝わり、即時Viewに反映されます。
```swift
struct ContentView: View {
    @State var count = 0

    var body: some View {
        VStack {
            CountDisplay(count: count) // 渡す側
            Button("Count up") {
                count += 1
            }
        }
    }
}

struct CountDisplay: View {
    var count: Int // 渡される側

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

# 親Viewに状態の変更を伝えたい
子Viewで状態が変わったことを親Viewでも検知したい場合は、`@Binding`を使用します。
親Viewで`@State`を管理し、子Viewにバインドするというイメージです。
```swift
struct ContentView: View {
    @State var count = 0

    var body: some View {
        VStack {
            CountDisplay(count: count)
            CountButton(count: $count) // '$'をつけてバインドする
        }
    }
}

struct CountButton: View {
    @Binding var count: Int // 変更が親に伝わる

    var body: some View {
        Button("Count up") {
            count += 1
        }
    }
}
```

# カスタムオブジェクトを扱いたい場合
SwiftUIでは原則`struct`を使用することが推奨されています。
カスタムオブジェクトを実装する場合は可能な限りIntやStringなどのプリミティブ型で構成した構造体として実装します。
```swift
struct Person {
    var name: String
    var age: Int
    var isPremium: Bool
}
```
上記のような構造体は、これまでに説明した`@State`や`@Binding`を使用してView内での状態管理に含めることができます。

# クラスを扱いたい場合
何らかの理由でオブジェクトの定義を`class`として行う場合、`class`は参照型であるため、SwiftUIはプロパティの変更を検出することができません。
`class`を状態として管理したい場合は、そのclassを`ObservableObject`に準拠させる必要があります。
さらに、SwiftUIで変更を検出してほしいプロパティに`@Published`を付与します。
```swift
class Person: ObservableObject {
    @Published var name = ""
    @Published var age = 0
    @Published var isPremium = false
}
```
Viewで利用する際も、`@State`は使用せず、`@StateObject`を使用します。
```swift
struct ContentView: View {
    @StateObject var person = Person()

    var body: some View {
        HStack {
            Text(person.name)
            Text(person.age.description)
            Spacer()
            Text(person.isPremium ? "P" : "N")
        }
    }
}
```
また、子Viewに対しクラスのまま渡してしまった場合、子Viewはクラスの変更を検出できません。（即時反映されない）
あくまで`@StateObject`を付与したViewでのみ、変更がPublishされるのです。
これを回避するためには、子View側でも`@ObservedObject`を付与して、監視対象であることを明示してください。
```swift
struct ContentView: View {
    @StateObject var person = Person()

    var body: some View {
        PersonRow(person: person)
    }
}

struct PersonRow: View {
    var person: Person // これはダメ
    @ObservedObject var person: Person // これでOK

    var body: some View {
        HStack {
            Text(person.name)
            Text(person.age.description)
            Spacer()
            Text(person.isPremium ? "P" : "N")
        }
    }
}
```
# オプショナル型を扱いたい場合
ラップされる値が`struct`の場合、これまでの`struct`型と同様に`@State`を使用して状態管理することが可能です。
しかし、`class`の場合、さらに注意が必要となります。
まずオプショナル型は、歴とした一つの型であると考えてください。`Person?`は、Person型に?がついただけだと考えるのではなく、Personをラップしただけの**オプショナル型**です。
オプショナル型は、`struct`と同様に`@State`を使用して状態管理できますが、その代わり`@StateObject`や`@ObservedObject`を付与することはできません。
また、この時SwiftUIが検知するのは`Person`の変更ではなくオプショナル型としての変更であることに注意してください。
つまり、`Person?`の値が`nil`であるか、`Person`であるかの2択が変更された時にしか、Viewへの反映は行われないということです。Personの内容が変更されたことを反映するには、アンラップしたPersonクラスを状態管理する子Viewを作る必要があります。
```swift
struct ContentView: View {
    @State var person: Person?

    var body: some View {
        if let person { // アンラップする
            PersonRow(person: person)
        }
    }
}

struct PersonRow: View {
    @ObservedObject var person: Person // アンラップされたものを監視する

    var body: some View {
        HStack {
            Text(person.name)
            Text(person.age.description)
            Spacer()
            Text(person.isPremium ? "P" : "N")
        }
    }
}
```

# 配列を扱いたい場合
配列の場合も、中身が`struct`の場合は問題ありませんが、`class`の配列の場合は注意が必要です。
配列自体は、`struct`と同様に`@State`を使用して状態管理できますが、こちらも`@StateObject`や`@ObservedObject`を付与することはできません。
例えば`[Person]`を定義したとき、SwiftUIが検知するのは配列としての変更であり、配列内の要素である`Person`が変更されたとしてもViewに反映されることはありません。
配列の変更というのは、要素が追加された時、要素が削除された時、要素そのもの（IDなど）が完全に置き換わった時、順番が入れ替わった時などです。要素の内容変更をViewに反映したい場合は、要素ごとのPersonクラスを状態管理する子Viewを作る必要があります。
```swift
class Person: ObservableObject, Hashable {
    @Published var name = ""
    @Published var age = 0
    @Published var isPremium = false

    // 以下はHashableに準拠させるため(ForEachのため)に必要
    let id = UUID()
    func hash(into hasher: inout Hasher) {}
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.id == rhs.id
    }
}

struct ContentView: View {
    @State var persons: [Person] = []

    var body: some View {
        VStack {
            ForEach(persons, id: \.self) { person in
                // 要素を取り出す
                PersonRow(person: person)
            }
        }
    }
}

struct PersonRow: View {
    @ObservedObject var person: Person // 取り出されたものを監視する

    var body: some View {
        HStack {
            Text(person.name)
            Text(person.age.description)
            Spacer()
            Text(person.isPremium ? "P" : "N")
        }
    }
}
```

# グローバルなオブジェクトとして状態管理したい場合
宣言型UIの状態管理は、基本的にバケツリレーとなります。
つまり、親Viewから子View、孫Viewにかけて、必要なデータを渡し続けます。
しかしこのバケツリレーがあまりにも多かったり、中間階層のViewで全く使用しないオブジェクトは、いちいちViewの引数として定義したくない場合があります。
そんな時は`@EnvironmentObject`を使用します。
なお、`@EnvironmentObject`が付与できるのは、`@ObservableObject`のみであるため、別途クラスの作成が必要です。
```swift:MySettings.swift
class MySettings: ObservableObject {
    @Published var isUseFaceID = false
}
```
また、実際に使用するViewよりも上の階層で、インスタンスをセットしておく必要があります。一般的にはエントリーポイントである〇〇Appのファイルで記述されると思います。
```diff_swift:SampleApp.swift
@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
+               .environmentObject(MySettings())
        }
    }
}
```
```swift:ContentView.swift
struct ContentView: View {
    @StateObject var person = Person() // バケツリレーの場合

    var body: some View {
        ChildView(person: person)
    }
}

struct ChildView: View {
    var person: Person // 使わないけど渡される

    var body: some View {
        ChildChildView(person: person)
    }
}

struct ChildChildView: View {
    var person: Person // 使わないけど渡される

    var body: some View {
        ChildChildChildView(person: person)
    }
}

struct ChildChildChildView: View {
    @ObservedObject var person: Person // ここでやっと使う
    @EnvironmentObject var settings: MySettings // 使うとこで呼べる
    
    var body: some View {
        VStack {
            Text("Your name is \(person.name)")
            Toggle("Use Face ID", isOn: $settings.isUseFaceID)
        }
    }
}
```

# まとめ
とりあえず以上となります。他にも書くことがあれば追記します。
ご意見や修正案、ネタリクエストなどお気軽にコメントください！