Environment vs State

I’m taking a moment out of my coursework to try and get my head around Property Wrappers.

When writing PHP code in the past, variables/objects were either local or global. If you used them within a function, then they were local to that function. If you wanted to use a variable from outside the function or have it available outside that function, you’d declare it with the global keyword.

This was refered to as the ‘scope’ in which the variable was available. It started to get a little more complicated when I transitioned my code into OOP, but I didn’t get chance to complete my transition to OOP so my knowledge is limited.

With SwiftUI, we have “property wrappers” which appear to achieve the same ends, but my head is still hooked on the past. So I’m trying to get it clear in my head what we’re actually doing when we use property wrappers.

The main reason for the mental block in my head is because SwiftUI is centred around one or more ‘views’ that are subject to change. Being a web-based script, PHP code was generally there to create a static HTML page that didn’t change unless you refreshed the page and the entire PHP script got executed again. That’s not the case with an app where we need to act on user input or some other change instantly.

“Property Wrappers” are needed because a SwiftUI view is “immutable” (i.e., cannot be changed), like the aforementioned page generated by a PHP script. But a view can contain “mutable” data (data that can be changed). If the data is changed, the view needs to be “recreated” to reflect that change, otherwise the view won’t change.

A “property wrapper” is a way of letting SwiftUI know that data has changed, and so the view needs to be recreated.

As I understand it, the immutable View exists within a mutable State. The State can contain data and, if that data changes so does the State which then prompts the View to be recreated.

The Property Wrappers I’m going to attempt to summarise here are @State, @StateObject, @ObservedObject, and @EnvironmentObject, and only in the context in which we’ve used them so far in the coursework. I’m sure there is much more to learn about them.

@State

Local state value.

@State var score = 1

@State is for simple values (Int, String, Bool, etc), not for complex reference types (such as class, struct, etc). If the variable score changes within the local scope (such as from an action in a view within the state), then the body will be recreated because the State has changed.

Although @State is not for complex reference types, the following will not cause an error:

@State var obj = classObject()

However, it will also not cause the body to be recreated. This is because classObject() is not part of the State, only the variable obj is and the variable doesn’t see the change to the object, which means the State hasn’t change and so it does not trigger the body to be recreaed.

@StateObject

Local state object.

@StateObject var userSettings = classObject()

@StateObject is for an observable object rather than a simple value. The object classObject() has to conform to the ObservableObject protocol and the properties that should trigger a state change should be marked with a @Published property wrapper.

class classObject: ObservableObject {
    @Published var userEmail: String "a@b.com"
}

The view can update the state object, thus:

userSettings.userEmail = "c@d.com"

The view will be recreated because the state knows that the StateObject has changed because userEmail is @Published within an ObservableObject.

@ObservedObject

Child view accessing the properties of a parent’s state object.

@ObservedObject var obsObj: classObject

To access the properties of a StateObject in a child view (a view shown after a button tap, for example), the StateObject needs to be passed to that child view because the object has already been instantiated.

Apparently, @ObservedObject was previously known as @BindableObject (but I don’t know anything about that).

The main view with the StateObject may contain a link:

NavigationLink("To child", destination: ChildView(obsObj: userSettings))

The child view accesses the properties of the object via an @ObservedObject wrapper, which accepts an object of type classObject as previously declared.

The child view may then update a property of the observed object, thus:

obsObj.userEmail = "e@f.com"

This updates the property of the StateObject that was instantiated and is used by the parent view.

@EnvironmentObject

All sub-views accessing a global object.

@EnvironmentObject var envObj: classObject

The environment encompasses all local states (views) within it. An environment object can therefore be used by all states it encompasses, but the object needs to be declared in the top-most view in order to be accessible.

Within @main we would append the .environmentObject modifier to the view that calls all sub-views, thus:

@main
    struct TestApp: App {
        @StateObject var envObj = classObject()
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environmentObject(envObj)
            }
        }
    }

To then use the object in ContentView() and any view within ContentView(), we’d use this:

@EnvironmentObject var envObj: classObject

And to update the object, we’d use:

envObj.userEmail = "g@h.com"

This is just like @ObservedObject except that we’re expecting the object to be in the Environment, rather than being passed from the parent view. If there are a lot of objects that may be passed to many views and sub-views, having them available from the Environment would be better than passing them all as @ObservedObject.

@Binding

On trying to sort the above out in my head, I’ve come across other wrappers including @Binding. Although we have used “two-way binding” using the string/dollar ($) sign, we haven’t yet used the @Binding property wrapper so I won’t try to detail it here at this time.

The above is for my own reference, and I make no apologies for any inaccuracies. I will no doubt update things as I learn more.