SwiftUI: Meet the most important property wrappers for a View's data refresh
As you may know(or discover now), there is a bunch of property types for a View structure in SwiftUI and according to this type, the View's lifecycle and way of refreshing should be different. As a View is a struct, it's never actually updated as an object, but instead it is recreated as a new instance every time it should be refreshed. The so called state variables, when changed, force the application to re-render a new screen(or subview) in order to update the interface according to the new data. Think of that as the UI you see on your device was a function taking the properties as parameters:
iPhone_Interface = f(x,y,z)
Your view relies on x, y and z values to define what is actually displayed on screen. But can you identify what is different from the variables x and y, declared with the State property wrapper and z? Let's start by explaining that
State variables x Injected properties
As you might have already noticed, you don't define constructors for Views, neither to any other value type by default. When you define just a simple variable, may it be a let or var, it's simply injected into your View when it's initialized and its display may take it as a parameter to show whatever it's supposed to, may it be a text, or an image, or whatever.
When you tag a property as a State, it's initialized with a default value, and when it changes, you are saying to your application that the old interface is not valid anymore, so it must be rendered once more:
In this example, once the button is clicked, the state variable text is updated, and so the View should be reinstantiated with the new value. It forces the refresh.
The same could not be said if text wasn't a state variable. If it doesn't have the property wrapper, it doesn't refresh again. It would be only an injected value used from the first time.
StateObject x ObservedObject
Now that we defined what the State wrapper does, let's distinguish these two similar concepts regarding an object that is used to define my interface. You may think of a StateObject as the same way of the state variables with simple types, like integers, strings, etc. But, as the name says, we are actually dealing with a reference type, which value is changed in its memory address wherever it's updated:
Repair that, as we are watching for the object state and we update our interface when it changes, the class shall implement the ObservableObject protocol and all the variables we want to keep track must be tagged as Published. This way, when the properties change, the View who observes them is notified. Quick note: we can produce very practical results by customizing our published variables with the Combine framework, transforming, filtering and mapping errors.
As the state object is a reference type, did you think about making other Views to watch its state and be updated depending on the values? It should keep all the Views synced! As this object was created by an original View, this View is responsible to updated the observed object depending on some events.
If you want to make other Views listen to it, just add this same object as a property to them tagging it as an ObservedObject:
The two subviews receive the StateObject from the SuperView and when the superview updates it, the subviews that rely on it as an ObservedObject should be refreshed. The StateObject variable is the original reference whose value is updated.
Another alternative for a complex hierarchy: EnvironmentObject
Environment Object may be seen as a great alternative for sharing an object across a huge View hierarchy. The difference from the previous case with the StateObject/ObservedObject example is that the environment object doesn't need to be passed when initializing subviews within the hierarchy that had it injected. Just declaring this object with the same name and tagging with the EnvironmentObject property wrapper is enough. Imagine the same example as before with the SuperView and two subviews:
Repair that now we are just "injecting" the environment object into the hierarchy's root with the environmentObject modifier and the other views(including the root) don't need it in the constructor. All the subviews have access to the shared object by just declaring it and the interface is refreshed once the object is updated. Imagine you have a very complex scene with a lot of subviews, children and simblings. Isn't that much simple to hold all the states in a single place and everything is kept synced once it's modified? When we used UIKit, everything should be updated with delegation and completions, leading us to a huge chain of events. SwiftUI solves us this problem.
Notice the result is the same:
EnvironmentObject only problem
SwiftUI blesses us with the power of reusing a View in any context and testing that in an isolated fashion through the editor previews, but there is just one side effect to this when using environment variables: If we initialize a View that expects an environment object that wasn't actually provided. The application will bring this crash:
It limits us from reusing the Subview in any context, any screen. It needs to be inserted in a place that provides this environment object.
In this article we explained that a View's rendering is actually a function of its state properties and it should be refreshed every time it's updated. We have the state properties that represent raw types(Int, String, Double..), State Objects that represent reference types and can be shared across other views and in there it's seen as an ObservedObject. And finally, we checked the EnvironmentObject, which allows us to share some state within a whole hierarchy of views by just providing it to the root. With these mechanisms, we can easily avoid a bunch of delegates and closures in other to keep synchronization and everything keeps integrity. I hope you enjoyed ;)