Closed1

SwiftUI のState管理

hirothingshirothings

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内のデータ更新に使う
  • @Binding: 値を複数Viewで参照し、双方向で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でメンバ変数としてインスタンス化してから渡す

参考記事

このスクラップは2022/02/08にクローズされました