SwiftUI のState管理
State管理早見表
- 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でメンバ変数としてインスタンス化してから渡す