🦁

SwiftUI Observable(var, State, Bindable, Environment)の使い方と違い

2023/06/11に公開1

注意事項

9月頃正式リリースであるXcode15(iOS 17)の機能の記事なので変更がある今後変更される可能性があります。

@Observable

@Observableとはマクロで実装されている機能で、モデル(値)の変更を監視して、View側に伝えることができる機能です。

単純なプロトコルのObservableとは異なります。

Observableの使い方

isPresentdが変更されるとRectangle()の表示/非表示が切り替わります。

@Observable
final class ViewModel {
  let color: Color
  var isPresented = false
  
  init(
    color: Color,
    isPresented: Bool
  ) {
    self.color = color
    self.isPresented = isPresented
  }
}

struct SubView: View {
  @State var viewModel: ViewModel
  
  var body: some View {
    VStack {
      Toggle("Toggle", isOn: $viewModel.isPresented)
    
      if viewModel.isPresented {
        Rectangle()
          .fill(viewModel.color)
          .frame(width: 100, height: 100)
      }
    }
  }
}

struct ContentView: View {
  var body: some View {
    SubView(viewModel: ViewModel(color: .red, isPresented: true))
  }
}

違い(var, State, Bindable, Enviroment)

ObservableObjectを知っている人向けの説明

以下のような対応になっています。動きもObservableObjectの時と同じです。

  • State(StateObject)
  • Bindable(ObservedObject)
  • Enviroment(EnvironmentObject)
  • var(Bindingができない)
  • State(親Viewの変更を監視しない)
  • Bindable(親Viewの変更を監視する)
  • Enviroment(Bindableと同じ。設定したView下全てのViewで参照可能)

Bindingができない

TextFieldなどの子View先で@Observableでない値の変更が必要な場合Bindingが必要です。(@Observableな値を渡す場合はBindingは必要なし)
下の例ではStringが@ObservableではないのでBindingが必要です。

この場合ただのvarだと不可能です。@State, @Bindableなどが必要です。

@Observable
final class ViewModel {
  var text: String
  
  init(text: String) {
    self.text = text
  }
}

struct ContentView: View {
 //@State
  var viewModel = ViewModel(text: "Hello World!")
  
  var body: some View {
    TextField("TextField", text: $viewModel.text)
  }
}

親Viewの監視とは(State/ Bindable)

ContentViewがStateViewとBindableViewを表示させています。
StateViewとBindableViewの違いはviewModelの持ち方のみです。

ContentViewのButton("Change")を押すとStateViewには変化がありません。これはStateViewの親ViewであるContentViewの変更の監視をしていないためです。

一方BindableViewは親ViewであるContentViewの変更を監視しているため、BindableViewに変更を伝えられています。

@Observable
final class ViewModel {
  let id: String
  var isPresented = false
  
  init(id: String) {
    self.id = id
  }
}

struct BindableView: View {
  @Bindable var viewModel: ViewModel
  
  var body: some View {
    VStack {
      Text(viewModel.id)
      Text(viewModel.isPresented ? "True" : "False")
    }
  }
}

struct StateView: View {
  @State var viewModel: ViewModel
  
  var body: some View {
    VStack {
      Text(viewModel.id)
      Text(viewModel.isPresented ? "True" : "False")
    }
  }
}

struct ContentView: View {
  @State var id = "100"
  
  var body: some View {
    VStack {
      StateView(viewModel: .init(id: id))
        //.id(id)
      BindableView(viewModel: .init(id: id))
      Button("Change") { id += "0" }
    }
  }
}

Environmentの特性

ContentView -> View1 -> View2 -> View3 -> EnvironmentViewのような深い階層のViewがあったときにContentView ->EnvironmentViewにObjectを伝搬する際に、引数で渡していくと大変です。

それにEnvironmentを使うとView1, View2, View3はObjectの事をを知らずとも、EnvironmentViewに伝えることが可能です。

逆もまた可能です。最後のViewであるEnvironmentViewからContentViewに値を伝えることも難しいです。そこでもEnvironmentの利用できます。

@Observable
final class ViewModel {
  var isPresented = false
}

struct EnvironmentView: View {
  @Environment(ViewModel.self) var viewModel
  
  var body: some View {
    Button("Toggle") {
      viewModel.isPresented.toggle()
    }
  }
}

struct View1: View {
  var body: some View {
    VStack {
      Text("View1")
      View2()
    }
  }
}

struct View2: View {
  var body: some View {
    VStack {
      Text("View2")
      View3()
    }
  }
}


struct View3: View {
  var body: some View {
    EnvironmentView()
  }
}

struct ContentView: View {
  @State var viewModel = ViewModel()
  
  var body: some View {
    VStack {
      Text(viewModel.isPresented ? "True" : "False")
      View1()
    }
      .environment(viewModel)
  }
}

使い分け(優先度)

基本的には他のViewへの影響度が少ないただのvarが安全です。(1つのViewの責任を小さくするため)
Bindingが必要であればStateを使うという形です。
Environmentは距離が遠いViewでの利用がおすすです。
Bindableを使うべき場面はあまりありません。(StateとView.id()の組み合わせがあるため)

パフォーマンス良い順ではState >- Bindable >- Enviromentだと思うのですが、@StateObject, @ObservedObjectの時点では大きいプロジェクトでもあまりパフォーマンスは気にならなかったです。(厳密な検証はできていません)

Discussion

tana00tana00

Observable Protocolを使うならObservable Macroを使うべし!とDocumentationに記載があった。そのせいか、この記事のコードはApp Projectでは動作するのだが、Playgroundではコンパイル出来ない。
Playgroundがマクロをサポートしていない事が原因なのかも!
記事の一部のコードはObservable Protocolを使えば動作する。

$xcodebuild -version
Xcode 15.3
Build version 15E204a
$sw_vers
ProductName:		macOS
ProductVersion:		14.4.1
BuildVersion:		23E224

@Observable not working in Xcode p… | Apple Developer Forums
Migrating from the Observable Object protocol to the Observable macro | Apple Developer Documentation