SwiftUI のState管理
 hirothings
hirothingsState管理早見表
- State: 単一方向のデータ更新ができる。View内のデータ更新に使う
- Binding: 双方向のデータ更新ができる
- StateObject: ObservableObjectをView, App, Sceneで保持するときに使う
- ObservedObject: 親ViewのObservableObjectを子で保持する場合使う
- EnvironmentObject: アプリの状態を管理するObservableObjectを伝搬できる
- Environment: アプリの状態を伝搬できる(カラー, 画面サイズなど)
- AppStorage: UserDefaultsを使いやすくしたProperty Wrapper
- SceneStorage: シーン単位で管理できる(あまり使わなさそう)
- PreferenceKey: 子から親にデータを伝搬できる
@Stateと@Bindingの違い
@State
単一方向のデータ更新に使う。値をコピーするため、複数Viewで同じObjectを参照するような使い方はできない
ローカルでしか使用しないデータのアクセスに適している
@Binding
値を参照する。そのため双方向でデータの更新ができる
struct PlayerView: View {
    var episode: Episode
    @State private var isPlaying: Bool = false
    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(isPlaying: $isPlaying)
        }
    }
}
struct PlayButton: View {
    @Binding var isPlaying: Bool
    var body: some View {
        Button(action: {
            // isPlayingの値は親にも伝わる
            self.isPlaying.toggle()
        }) {
            Image(systemName: isPlaying ? "pause.circle" : "play.circle")
        }
    }
}
前提: ObservableObjectとは
@State, @Bindingなどと違い、複数の状態を保持することができる
class Contact: ObservableObject {
    @Published var name: String
    @Published var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    func haveBirthday() -> Int {
        age += 1
        return age
    }
}
@ObservedObject
@StateObjectなどObservableObjectを下位ツリーで受け取る場合は、@ObservedObjectを使用する
View自身でインスタンスを保持している場合、@ObservedObjectのライフサイクルはbodyが更新されるまで
そのためView自身で保持するデータオブジェクトに@ObservedObjectを付与するとそのデータオブジェクトは頻繁に再生成されてしまう。View自身で保持するObservableObjectには@StateObjectを使うこと
再生成されずインスタンスを保持できる例
struct ParentView: View {
    @State private var dataSource = DataSource() // ObservableObject
    var body: some View {
        ChildView(dataSource: dataSource)
    }
}
struct ChildView: View {
    // 親Viewのbodyの外側にある@Stateを@ObservedObjectに渡しているので、親Viewのbodyの再描画に影響しない
    @ObservedObject var dataSource: DataSource
    var body: some View {
        VStack {
            Button("increment counter") {
                dataSource.counter += 1
            }
            Text("count: \(dataSource.counter)")
        }
    }
}
bodyの更新のたびに再生成され、インスタンスを保持できない例
この場合、@StateObjectを使う
// たびたびレンダリングされるBody内でObservedObjectを親から子に渡しても保持されない
struct ParentView: View {
    var body: some View {
        ChildView(dataSource: DataSource())
    }
}
// 子View内のプロパティでObservedObjectを初期化しても親Viewのbodyの再生成の影響を受け保持されない
struct ChildView: View {
    @ObservedObject var dataSource = DataSource()
    var body: some View {
        VStack {
            Button("increment counter") {
                dataSource.counter += 1
            }
            Text("count: \(dataSource.counter)")
        }
    }
}
@StateObject
View内でObservableObjectを管理する場合、@StateObjectを使う
ライフサイクルはViewが表示されてから消えるまで(.onAppearから.onDisAppearまで)
宣言時に初期化している例が多いが、DIすることも可能。
DIするならObservedObjectを使えば良いように思うが、Viewがrebuildするたびに再生成される仕様なので用途が合わない場合がある
アプリ全体でStateObjectを共有することもできる(Share an Object Throughout Your App)
@main
struct BookReader: App {
    @StateObject var library = Library()
    
    var body: some Scene {
        WindowGroup {
            LibraryView()
                // environmentObjectで下位ツリーに共有
                .environmentObject(library)
        }
    }
}
DIしながら初期化する方法
struct HomeView: View {
  @StateObject viewModel: HomeViewModel
}
HomeView(viewModel: HomeViewModel(userId: "aaa"))
こう初期化することもできる。ドキュメントでは直接呼ばないよう明言されているが使う
struct HomeView: View {
  @StateObject viewModel: HomeViewModel
  
  init(userId: UserId) {
    self._viewModel = StateObject(wrappedValue: HomeViewModel(userId: userId))
  }
}
@StateObjectと@ObservedObjectの違い
ライフサイクル
View自身でインスタンスを保持している場合、ライフサイクルが違う
- @StateObject: Viewが表示されてから消えるまで(.onAppearから.onDisAppearまで)
- @ObservedObject | @EnvironmentObject: Viewのbodyが再生成されるまで
ライフサイクル詳細
- ObservedObjcet, EnvironmentObjcet: 親ViewのリビルドでBodyが再生成されるため、破棄される
- StateObject: Viewのリビルド自体はされるが、StateObjectのインスタンスは保持される→UIKit同様Viewのdeinit時に、破棄される挙動になる
データの正しい渡し方
- @StateObject: 自身のViewで初期化 or 親ViewからDIする
- @ObservedObject: 親Viewでメンバ変数としてインスタンス化してから渡す

