1

I know we can pass custom ObservableObjects types using the .environmentObject(_:) and then access it from subviews using @EnvironmentObject special properties.

But what it we want to pass non-custom, standard Int, String properties around views?

The only candidate I can see is:

func environment<V>(_ keyPath: WritableKeyPath<EnvironmentValues, V>, _ value: V) -> some View

But it seems that it only works for fixed, non-custom, KeyPaths such as \.colorScheme.

In other words I am looking to pass around a @State using the Environment.

Rivera
  • 10,792
  • 3
  • 58
  • 102
  • 3
    A `@State` should be bound to the `View` it belongs to. Why do you need to pass an `@State`? Wrap the `Int` or `String` in an `ObservableObject` and pass that – krjw Nov 12 '19 at 12:37

2 Answers2

5

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.

John M.
  • 8,892
  • 4
  • 31
  • 42
0

When it is needed, I pass @State via arguments or constructor as Binding. You can find wide usage of this approach in my code for post How do I create a multiline TextField in SwiftUI?

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • The idea to use the Environment is to avoid passing objects along subviews deep in the hierarchy where they will be actually needed. – Rivera Nov 12 '19 at 16:11