IntroductionSwiftUI differs from Apple’s previous UI frameworks not only in how views and other UI components are defined, but also in how the state of the view hierarchy is managed throughout an app that uses it. Rather than using delegates, data sources, or any other state management patterns common in imperative frameworks like UIKit and AppKit, SwiftUI comes with a few property wrappers[1] that allow us to declare exactly how our data should be observed, rendered, and mutated by our views. This week, let’s take a closer look at each of these property wrappers, how they relate to each other, and how they form different parts of SwiftUI’s overall state management system. Property StatusSince SwiftUI is primarily a UI framework (although it’s also starting to get APIs for defining higher-level structures like apps and scenes), its declarative design doesn’t necessarily need to affect the entire model and data layer of our app — but rather just bind directly to the state of our various views. For example, let's say we're developing a SignupView that enables users to register a new account in our application by entering a username and email address. We'll use these two values to form a User model and pass it to a closure: struct SignupView : View { Since only two of these three properties — username and email — will actually be modified by our view, and these two states can remain private, we’ll mark them using SwiftUI’s State property wrapper — like this: struct SignupView : View { Doing this will automatically create a connection between these two values and our view itself - meaning our view will be re-rendered every time these two values are changed. In our body, we will bind each of these two properties to a corresponding TextField to make them editable by the user: struct SignupView : View { State is therefore used to represent the internal state of a SwiftUI view and automatically cause the view to update when that state is changed. As such, it is most common practice to keep State property wrappers private, which ensures that they are only changed within the body of that view (trying to change them elsewhere will actually cause a runtime crash). Two-way bindingLooking at the code sample above, the way we pass each property into its TextField is by prefixing those property names with $. This is because we are not just passing in plain String values into those text fields, but are binding to our State wrapped properties themselves. To explore what this means in more detail, let's now assume that we want to create a view that lets our users edit the profile information they originally entered when they signed up. Since we're now modifying external state values, rather than just private state values, this time we mark the username and email properties as Bingding: struct ProfileEditingView : View { The cool thing is that bindings aren’t just limited to single built-in values, like strings or integers, but can be used to bind any Swift value to one of our views. For example, instead of passing two separate username and email, we can pass the User model itself to the ProfileEditingView: struct ProfileEditingView : View { Just like we prefixed the State and Binding wrapped properties into the various TextField instances with $, we can do the same thing when connecting any State value to our own defined Binding properties. For example, here is an implementation of a ProfileView that uses a Stage wrapper property to keep track of a user model, and then passes that model a binding when rendering an instance of the above ProfileEditingView as a sheet - this will automatically sync any changes the user makes to that original State property value: struct ProfileView : View { Note that we can also change a State-wrapped property by assigning a new value to it - like when we set isEditingViewShown to false in the action handler of the "Done" button. Thus, a Binding-tagged property provides a two-way connection between a given view and a state property defined outside of that view, while both Statr and Binding-wrapped properties can be passed as bindings by prefixing their property name with $. Observation objectWhat State and Binding have in common is that they deal with values that are managed within the SwiftUI view hierarchy itself. However, while it is certainly possible to build an app that keeps all of its state within its various views, it’s generally not a good idea from an architectural and separation of concerns perspective, and can easily lead to our views becoming quite large and complex. Thankfully, SwiftUI also provides some mechanisms that allow us to connect external model objects to our various views. One of these mechanisms is the ObservableObject protocol, which, when combined with the ObservedObject property wrapper, allows us to set up bindings to reference types that are managed outside of our view layer. As an example, let’s update the ProfileView defined above — by moving the responsibility for managing the User model from the view itself into a new, specialized object. Now, we could describe such an object in many different ways, but since we’re looking to create a type to control an instance of one of our models — let’s make it a model controller [2] that conforms to SwiftUI’s ObservableObject protocol: class UserModelController : ObservableObject {
With the above types in place, let's now go back to our ProfileView and have it observe our new UserModelController instance as an ObservedObject instead of keeping track of our user model with a State property wrapper. Best of all, we can still easily bind this model to our ProfileEditingView just like before, since ObservedObject property wrappers can also be converted to bindings: struct ProfileView : View { However, one important difference between our new implementation and the state-based implementation we used previously is that our UserModelController now needs to be injected into the ProfileView as part of its initializer. Besides "forcing" us to have a more explicit dependency graph in our codebase, the reason is that a property marked with ObservedObject does not imply any kind of ownership over the object that this property points to. So while the following might technically compile, it will ultimately cause issues at runtime - because when our view is recreated when it is updated, the UserModelController instance may be deleted (since our view is now its primary owner): struct ProfileView : View {
To solve the above problems, Apple introduced a new property wrapper in iOS 14 and macOS Big Sur called StateObject. Properties marked as StateObject behave exactly like ObservedObject-in addition, SwiftUI will ensure that any objects stored in such properties will not be accidentally released because the framework re-creates a new instance when the view is re-rendered: struct ProfileView : View { Even though you can technically only use StateObject from now on - I still recommend using ObservedObject when observing external objects, and only using StateObject when dealing with objects owned by the view itself. Think of StateObject and ObservedObject as reference types to State and Binding, or the SwiftUI version of strong and weak properties. View and modify environment variablesFinally, let’s look at how SwiftUI’s environment system can be used to pass various states between two views that aren’t directly connected to each other. While it’s usually easy to create a binding between a parent view and one of its children, passing an object or value throughout a view hierarchy can be quite cumbersome — and this is exactly the type of problem that environment variables are designed to solve. There are two main ways to use the environment with SwiftUI. One is to first define an EnvironmentObjectwrapped property in the view from which you want to retrieve a given object — for example, like this ArticleViewhow to retrieve a Theme object containing color information: struct ArticleView : View { We then have to make sure to provide our environment object (in this case a Theme instance) in one of our view’s parent classes, and SwiftUI will take care of the rest. This is done by using the environmentalObject modifier, for example, like this: struct RootView : View { Note that we don't need to apply the above modifier to the exact view that will use our environment object - we can apply it to any view above it in our hierarchy. The second way to use the SwiftUI environment system is to define a custom EnvironmentKey — which can then be used to assign and retrieve values from the built-in EnvironmentValues type: struct ThemeEnvironmentKey : EnvironmentKey { With the above in place, we can now mark our view's theme property with the Enviroment property wrapper (instead of EnvironmentObject ), and pass in the key value path of the environment key we wish to retrieve: struct ArticleView : View { One obvious difference between the two approaches described above is that the key-based approach requires us to define a default value at compile time, while the EnvironmentObject-based approach assumes that such a value is provided at runtime (failure to do so will result in a crash). summaryThe way SwiftUI manages state is definitely one of the most interesting aspects of the framework, and it may require us to slightly rethink the way data is passed around in our apps — at least when it comes to data that will be directly consumed and modified by our UIs. I hope this guide has been a good way to get an overview of SwiftUI’s various state handling mechanisms, and while some of the more specific APIs have been left out, the concepts highlighted in this post should cover the vast majority of use cases for all SwiftUI-based state handling. Thanks for reading! References[1] Property Wrappers: https://www.swiftbysundell.com/articles/property-wrappers-in-swift [2] Model Controller: https://www.swiftbysundell.com/articles/model-controllers-in-swift/ |
<<: Research on audio and video playback technology in Android system
>>: Transaction Gold Link Apollo Mobile Terminal Abnormal Monitoring System
The secret of increasing followers of millions of...
When all industries are digging for gold in the p...
B-end operation is an unfamiliar field for many p...
The current TV drama ratings remain sluggish, whi...
In mystery novels, there is a classic "Snows...
In order to earn a monthly salary of 18,000 yuan,...
In this article, the author will describe Douyin’...
If you ask, what flower symbolizes love? Everyone...
The course comes from a collection of multiple tu...
2021 From the beginning of the year to the end of...
The Alipay app, which has hundreds of millions of...
Since when, the Android operating system has beco...
Previously, we brought you the first preview of t...
The story of how Xiaomi started with 100 seed use...