A guide to SwiftUI’s state management system

Tram Ho

What separates SwiftUI from Apple’s previous UI frameworks is not just how views and other UI components are defined, but also how view-level state is managed within an app. Instead of using a delegate, datasource, or any other state management patterns commonly found in mandatory frameworks like UIKit and AppKit – SwiftUI provides several property wrappers that allow us to accurately declare how data is observed, rendered, and mutated. by our view. In this article, take a closer look at each of those property wrappers, how they relate to each other, and how they form the different parts of the overall state management system in SwiftUI.

State properties

Since SwiftUI is primarily a UI framework (although it starts to have APIs for defining higher-level constructs, like apps and scenes), its declarative design doesn’t necessarily affect the entire model and data layer. of an application – but rather just the state directly bound to our various views. For example, let’s say we are working on SignupView which allows a user to register a new account in an application, by entering a username and email . We’ll then use those two values ​​to form the User model, which is passed to the handler closure – giving us three parts of the state:

Since only two of those three properties – username and email – will actually be modified according to our view, and since those two parts of state can be private, we’ll highlight both using the State property wrapper. of SwiftUI – as follows:

Doing so automatically creates a connection between those two values ​​and our own view – meaning our view will be re-rendered every time one of the two values ​​is changed. In the body , we’ll bind each of those two properties with a corresponding TextField to make them editable – giving us the following implementation:

So the State is used to represent the internal state of the SwiftUI view and to automatically update the view when that state is changed. Hence, to keep State -wrapped properties private , this ensures that they will only be modified in the view body (trying to change them elsewhere will actually cause a runtime crash).

Two-way bindings

Looking at the code sample above, the way we pass each of our properties into TextField is by prefixing those property names with $ . That’s because they don’t just pass pure String values ​​into TextFields. That, but also associated with the main State -wrapped properties. To explore in more detail what that means, let’s now assume that we want to create a view that allows users to edit the profile information they initially entered upon registration. Since we are currently looking to modify external state values, instead of just private values, we will mark our username and email as Binding:

Interestingly, bindings are not limited to single built-in values, such as Strings or integers, but can be used to associate any Swift value with one of our views. For example, here’s how we can actually pass our own User model into ProfileEditingView , instead of passing two separate username and email values:

Just like how we prefix State and Binding -wrapped properties with $ when passing them into different TextField instances, we can do the same thing when we concatenate any State value with the Binding property. We have also self-defined. For example, this is an implementation of ProfileView that tracks the User model with the State -wrapped property, then passes a link to that model when displaying an instance of ProfileEditingView as a sheet – which automatically synchronizes. User change period:

Thus, the Binding -marked property provides a bidirectional connection between a given view and a state property defined outside of that view, and both the State and Binding -wrapped properties can be passed as binding by prefixing. their attribute names are equal to $ .

Observing objects

What both State and Binding have in common is that they handle values ​​managed in the SwiftUI view hierarchy itself. However, while it is definitely possible to build an application that holds all of its states in different views – that’s usually not an architecturally good idea and isolating concerns and possibly this easily leads to our view becoming quite large and complex. Thankfully, SwiftUI also provides several mechanisms that allow us to connect external model objects to different views. One such mechanism is the ObservableObject protocol, when combined with the ObservedObject property wrapper, which allows us to establish associations with managed reference types outside of our view layer. For example, let’s update the ProfileView we defined above – by moving our User model management responsibility out of the view and into a new, dedicated object. Now, there are several different metaphors that we can use to describe such an object, but since we are looking to create a type that will control an instance of one of their models. Let’s make it a controller model that conforms to SwiftUI’s ObservableObject protocol:

With the above type now let’s go back to ProfileView and make it observe an instance of the new UserModelController as ObservedObject , instead of using the State -wrapped property to track the User model. What’s really neat is that we can still easily bind that model to our ProfileEditingView , just like before, since ObservedObject-wrapped properties can also be converted to bindings – like this:

However, the key difference between the new and the State-based implementation we’ve used before, is that the UserModelController now needs to be included in ProfileView as part of its constructor. The reason for that, in addition to it “forces” us to set a more clearly defined dependency graph in the code, but also a property marked with ObservedObject doesn’t imply any form of ownership. to the object that the property points to. So while things like the following can successfully compile, it can cause runtime issues – since the version of the UserModelController stored in our view can be allocated when our view is recreated in update process (since our view is now its main owner):

To fix the above problem, Apple introduced a new property wrapper as part of iOS 14 and macOS Big Sur named StateObject . A property marked with StateObject behaves exactly like an ObservedObject – with the addition of SwiftUI will ensure that any object stored in such a property will not be accidentally released when the framework recreates the New instance of a view when re-rendering it:

Although it’s technically only possible to use StateObject from now on – I still recommend using ObservedObject when observing external objects and only StateObject when dealing with objects owned by one view. Consider StateObject and ObservedObject as reference types equivalent to State and Binding , or the SwiftUI versions of strong and weak properties.

Observing and modifying the environment

Finally, let’s see how SwiftUI’s enviroment system can be used to pass different parts of state between two views that are not directly connected to each other. While it’s often easy to create a constraint between the main view and one of its child views, passing a certain object or value around in the entire view hierarchy can be quite complicated – and that’s exactly. the type of problem the enviroment aims to solve. There are two main ways to use SwiftUI’s enviroment. One is to start by defining an EnvironmentObject-wrapped property in the view that wants to access a certain object – for example, how this ArticleView retrieves a Theme object that contains color information:

Then we have to make sure to provide the enviroment object (the Theme example in this case) in one of its parent views, and SwiftUI takes care of the rest. That’s done using the environmentObject modifier, for example like this:

The second way to use SwiftUI’s enviroment system is to define a custom EnvironmentKey – which can then be used to assign and retrieve values ​​to and from the EnvironmentValues type:

With the above, we can now highlight the theme ‘s theme property using the Environment property wrapper (not EnvironmentObject) and pass the key path of the environment key for which we want to retrieve the value:

One notable difference between the above two methods is that the key-based method requires us to specify a default value at compile time, while the EnvironmentObject-based method assumes such a value. will be offered at runtime.

Hope the article will be useful to you

Reference: https://www.swiftbysundell.com/articles/swiftui-state-management-guide/

Share the news now

Source : Viblo