I’m working with the new @Observable macro in SwiftUI (iOS 17+). In some views, my view model works perfectly without wrapping it in @State. But in other views, I must wrap it in @State or updates don’t propagate correctly (like alerts not showing, UI not refreshing, etc.).
Here are two examples that look similar, but behave differently.
struct AddProductView: View {
private var vm = AddProductVM()
var body: some View {
@Bindable var dVM = vm
VStack {
if dVM.shouldShowAlert {
Text("Alert triggered")
}
}
.onChange(of: dVM.someTrigger) {
dVM.shouldShowAlert = true
}
}
}
This works fine. When someTrigger changes, shouldShowAlert is updated, and the UI reflects it.
⸻
❌ Example that requires @State
struct ProductDetailsView: View {
@State private var vm = ProductDetailsVM() // <-- Required!
var body: some View {
@Bindable var dVM = vm
VStack {
if dVM.showErrorAlert {
Text("Error!")
}
}
.onAppear {
// Causes the alert to be dismissed unless vm is @State
dVM.fetchProduct()
}
}
}
Without @State, the UI either doesn’t update or resets, especially after .onAppear or .task.
⸻
🧠 My Question: • When exactly do I need to wrap an @Observable view model in @State? • What SwiftUI behaviors cause the view to be recreated and thus reinitialize the view model?
When exactly do I need to wrap an
@Observable
view model in@State
?
The documentation says this clearly:
Create the source of truth for model data
To create and store the source of truth for model data, declare a private variable and initialize it with an instance of an observable data model type. Then wrap it with a
State
property wrapper. For example, the following code stores an instance of the data model type Book in the state variablebook
:struct BookView: View { @State private var book = Book() var body: some View { Text(book.title) } }
By wrapping the book with
State
, you’re telling SwiftUI to manage the storage of the instance. Each time SwiftUI re-createsBookView
, it connects thebook
variable to the managed instance, providing the view a single source of truth for the model data.
For "view models", they are typically the "source of truth" for their corresponding view, so they are typically @State
s in their corresponding view.
If a property you declare is not supposed to be a source of truth, you should not use @State
- e.g. if you expect a parent view to provide a value for that property (in which case the parent view would be the source of truth).
In both cases you showed, you clearly do not expect AddProductVM
to come from somewhere else - i.e. the AddProductView
owns the source of truth. The vm
property should be a @State
in both cases.
The first case "just so happens" to work because you are lucky, as I will explain below.
What SwiftUI behaviors cause the view to be recreated and thus reinitialize the view model?
Wherever you write AddProductView()
. But when will AddProductView()
get called? This is not possible to answer without a full example, and is an implementation detail anyway. You should not rely on the timing on which View.init
is called.
"Depending on the timing of init
" is the key thing that @State
helps you avoid. As you have experienced, without @State
, the body
of the view will be accessing a different object after the view's init
has been called a second time. With @State
, the subsequently-created Observable
objects will be simply discarded, and the body
will see the same object.
Even still, there is still a new object being unnecessarily created and discarded. This is why the documentation for @State
recommends initialising Observable
objects in task
or onAppear
:
struct ContentView: View { @State private var library: Library? var body: some View { LibraryView(library: library) .task { library = Library() } } }