TL;DR
@State
is designed to be used in a single view (or passed via Binding
to a descendant view). It is just a convenience to avoid making an ObservableObject
class for every view. But if you are passing data to multiple views that are not tightly connected, you should wrap your data in an ObservableObject
class and pass around with .environmentObject()
Explanation
To elaborate on krjw's comment, and why you can't pass around @State
properties, let's talk about what the @State
, @ObservedObject
, @EnvironmentObject
, @Published
, and @Binding
property wrappers are actually doing.
Because your view is a struct, it is a value type. When your app's state changes, and views must be shown differently as a result, your current view struct is thrown away, and a new one created in its place. But this means that data cannot be stored inside the struct, because it would get thrown away and replaced by an initial value every time the view changes.
Enter the ObservableObject
protocol. When you create an object that conforms to this protocol, and mark one of its properties @Published
, this is just nice syntactic sugar to say, "I've got this reference object, which I intend to represent state; please set up some Combine publishers that broadcast any time there's a change." Because it's a reference type, it can sit somewhere in memory, outside of our views, and as long as our views have a reference to it and subscribe to its publisher, they can know when it changes, and refresh accordingly.
But remember, we said that our views are structs. Even if you assigned one of their properties a reference to the ObservableObject
and subscribed to its publishers, we would lose that information whenever the struct was recreated. This is where the @ObservedObject
wrapper comes in. In effect, all that's doing is saying, "Store the reference and subscription to this object outside my struct, and when my view is recreated, make sure I know I'm supposed to reference and listen to this object."
The @EnvironmentObject
does the same thing, it is just nice syntactic sugar to avoid having to pass in an ObservedObject
via initializer. Instead, it says, "Trust me, one of your ancestors will have an object of this type. Just go ask them for it."
So really, that would be enough to do everything we need to do in SwiftUI. But what if I just need to store and watch a Bool
for changes, and only need it in a single view? It's a bit heavy handed to make a whole class type just for that. That's where the @State
property wrapper comes in. It is basically just combining the functions of the ObservedObject
/@Published
stuff with the @ObservedObject
stuff. It says, "Keep this data outside my struct's memory (so it isn't thrown away on refresh), and notify me when anything changes." That's why you can modify your @State
properties, even though modifying a struct's properties should recreate the struct; the data is not really inside your struct.
@State
is just a condensed, convenient way to get the functionality of an ObservableObject
class with a @ObservedObject
wrapper, that will only be used in one struct.
For completeness, let's talk about @Binding
. Sometimes, a direct descendent of our view may need to modify the @State
variable of its ancestor (e.g., a TextField
needs to modify the String
value held by its parent view). The @Binding
property wrapper just means, "You don't need to store this data (because its already stored by another view/object); you just need to look at it for changes and be able to write changes back."
NOTE: Yes, under the hood, @State
is not literally setting up a class conforming to ObservableObject
and another @ObservedObject
wrapper; its much more efficient than that. But functionally, from a usage perspective, that's what's going on.