SwiftUI Observable(var, State, Bindable, Environment)の使い方と違い
注意事項
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
Observable Protocolを使うならObservable Macroを使うべし!とDocumentationに記載があった。そのせいか、この記事のコードはApp Projectでは動作するのだが、Playgroundではコンパイル出来ない。
Playgroundがマクロをサポートしていない事が原因なのかも!
記事の一部のコードはObservable Protocolを使えば動作する。
@Observable not working in Xcode p… | Apple Developer Forums
Migrating from the Observable Object protocol to the Observable macro | Apple Developer Documentation