03. Basic Concepts of SwiftUI (3)

It will take about 5 minutes to finish reading this article.

This article focuses on some common property wrappers in SwiftUI.

1. @State

In Swift, a computed property is a property that does not have a directly stored value, it is a calculated property value. Therefore, the mutating keyword is not allowed to modify computed properties. So code like the following is not allowed:

1
mutating var body: some View

However, in SwiftUI, a special solution called property wrapper @State is provided, which is not only used to create mutable properties that can respond to user interaction and state changes, but also to share and pass data between views.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import SwiftUI

struct ContentView: View {
@State private var counter: Int = 0

var body: some View {
VStack {
Text("Counter: \(counter)")
.font(.largeTitle)

Button("Increment") {
counter += 1
}
}
}
}

@State allows us to bypass the limitations of structs: we know their properties cannot be changed because the struct is fixed, but @State allows SwiftUI to store that value alone where it can be modified. Let’s take a look at its implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {

/// Creates the state with an initial wrapped value.
public init(wrappedValue value: Value)

/// Creates the state with an initial value.
public init(initialValue value: Value)

/// The underlying value referenced by the state variable.
public var wrappedValue: Value { get nonmutating set }

/// A binding to the state value.
public var projectedValue: Binding<Value> { get }
}

(1) @propertyWrapper:
This is a property tag indicating that State is a property wrapper.

(2) Follow the DynamicProperty protocol:
This protocol is a special protocol used to indicate that the value of a property may change dynamically at runtime. This protocol completes the interface needed to create dependent operations on data (state) and views. Only a few interfaces are exposed right now, and we can’t fully use it for now.

1
2
3
4
5
6
7
public protocol DynamicProperty {

/// Called immediately before the view's body() function is
/// executed, after updating the values of any dynamic properties
/// stored in `self`.
mutating func update()
}

(3) Its projectedValue (projected value) is of Binding type. Binding is a first-level reference to data. It serves as a bridge for two-way binding of data (state) in SwiftUI, allowing data to be read and written without owning the data. We will introduce it separately later.

1
@frozen @propertyWrapper @dynamicMemberLookup struct Binding<Value>

2. @binding

In SwiftUI, @Binding is used to create a two-way-bindable property, which allows properties to be bound to mutable state in other views in order to share and synchronize data between multiple views.

Because sometimes we will pass a property of a view to a child node, but it cannot be directly passed to the child node, because the value transfer form in Swift is the value type transfer method, that is, a copy is passed to the child node past value.

But after being modified by the @Binding decorator, the attribute becomes a reference type, and the transfer becomes a reference transfer, so that the state of the parent-child view can be associated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import SwiftUI

struct ContentView: View {
@State private var count: Int = 0

var body: some View {
VStack {
Text("Count: \(count)")
.font(.largeTitle)

ChildView(count: $count)
}
}
}

struct ChildView: View {
@Binding var count: Int

var body: some View {
Button("Increment") {
count += 1
}
}
}

@Binding actually encapsulates a reference to a value and provides an interface for reading and writing the value. When the bound value changes, the binding will automatically notify its dependencies, thereby triggering updates and re-rendering.

3. @ObservableObject

@ObservableObject is used to create observable objects that can be used by multiple independent Views. If you use @ObservedObject to decorate an object, then that object must implement the ObservableObject protocol, and then use @Published to decorate the property in the object, indicating that this property needs to be monitored by SwiftUI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import SwiftUI

class UserData: ObservableObject {
@Published var name: String = "John"
@Published var age: Int = 30
}

struct ContentView: View {
@ObservedObject var userData = UserData()

var body: some View {
VStack {
Text("Name: \(userData.name)")
.font(.largeTitle)

Text("Age: \(userData.age)")
.font(.largeTitle)

Button("Change Data") {
userData.name = "Jane"
userData.age = 25
}
}
}
}

In this example, we create a custom class called UserData and mark its properties name and age as observable using the @Published property wrapper. This means that when these properties change, SwiftUI will automatically post a notification, and the associated views will be notified and automatically updated to reflect the new value.

By using @ObservableObject and @Published property wrappers, we can create observable data models in SwiftUI, and realize dynamic updates of data and synchronization of views. This mechanism makes building responsive user interfaces easier and more efficient.

4. @StateObject

@State modifies value type data, and @StateObject is basically an upgraded version of @State for class.

The main function of @StateObject is to create an independent, observable object in the view and is responsible for managing the life cycle of the object.

Unlike @ObservedObject, @StateObject does not reinitialize the object when the view is recreated, but keeps its state unchanged. This makes sharing and managing observables in view hierarchies more convenient and reliable. Let’s compare it with an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import SwiftUI

class UserAuth: ObservableObject {
@Published var isLoggedIn: Bool = false
// Other user-related properties and methods...
}

struct ContentView: View {
@StateObject var userAuth = UserAuth()

var body: some View {
VStack {
if userAuth.isLoggedIn {
Text("Welcome, User!")
.font(.largeTitle)
} else {
LoginView(userAuth: userAuth)
}
}
}
}

struct LoginView: View {
@State private var username: String = ""
@State private var password: String = ""

@ObservedObject var userAuth: UserAuth

var body: some View {
VStack {
TextField("Username", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())

Button("Login") {
// Simulate authenticated users
if username == "admin" && password == "password" {
userAuth.isLoggedIn = true
}
}
}
}
}

In the above example, UserAuth is an ObservableObject that holds the logged-in state isLoggedIn. In the ContentView, we create a persistent UserAuth object using the @StateObject property wrapper and display a welcome message or a login view depending on the user’s login status.

Example of using @ObservableObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

import SwiftUI

class UserAuth: ObservableObject {
@Published var isLoggedIn: Bool = false
// Other user-related properties and methods...
}

struct ContentView: View {
@ObservedObject var userAuth = UserAuth()

var body: some View {
VStack {
if userAuth.isLoggedIn {
Text("Welcome, User!")
.font(.largeTitle)
} else {
LoginView(userAuth: userAuth)
}
}
}
}

struct LoginView: View {
@State private var username: String = ""
@State private var password: String = ""

@ObservedObject var userAuth: UserAuth

var body: some View {
VStack {
TextField("Username", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())

Button("Login") {
// Simulate authenticated users
if username == "admin" && password == "password" {
userAuth.isLoggedIn = true
}
}
}
}
}

In this example, we also use UserAuth as an ObservableObject to manage the user’s login status. The difference is that in the ContentView, we used the @ObservedObject property wrapper to create the UserAuth object. This means that whenever the ContentView is recreated, a new instance of the UserAuth object is created.

To sum up, @ObservedObject will be created multiple times with the creation of View regardless of storage, which is suitable for temporary and partial data models.

While @StateObject guarantees that the object will only be created once, it is suitable for objects that need to share and maintain persistent state in the entire view hierarchy.

Therefore, if it is an ObservableObject model object created by itself in View, using @StateObject will most likely be a more correct choice.

5. @EnvironmentObject

In SwiftUI, the main function of @EnvironmentObject is to share observable objects throughout the application and make them update automatically in views. By setting an observable as an environment variable, it is available throughout the view hierarchy without having to manually pass it to each view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import SwiftUI

class UserData: ObservableObject {
@Published var name: String = "John"
}

struct ContentView: View {
@EnvironmentObject var userData: UserData

var body: some View {
VStack {
Text("Name: \(userData.name)")
.font(.largeTitle)

Button("Change Name") {
userData.name = "Jane"
}
}
}
}

struct DetailView: View {
@EnvironmentObject var userData: UserData

var body: some View {
Text("Welcome, \(userData.name)!")
.font(.title)
}
}

struct AppView: View {
var body: some View {
ContentView()
.environmentObject(UserData())
}
}

@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
AppView()
}
}
}

In this example, we created a custom class called UserData and marked it as ObservableObject. We then use @EnvironmentObject property wrappers in ContentView and DetailView to set the UserData object as the environment object.

In ContentView, we can access the properties of the userData object and update them as needed. In the DetailView, we can also access the same userData object and use its properties to display the welcome message.

In the AppView, we use the environmentObject(_:) function to set the UserData instance as the application’s environment object so that it is shared across the application.

By using @EnvironmentObject property wrappers, we can share and access global observable objects throughout the application, making data transfer and synchronization easier and more convenient.

5. @FocusState

In SwiftUI, @FocusState is used to manage the focus state in a view. It allows you to track and control focus in the UI for automation if needed.

@FocusState provides a mechanism to track and control focus state in order to manage keyboard interactions and user input in the view hierarchy. It can be used to automatically switch focus between multiple text fields or views, and to perform focus-related actions such as submitting a form or handling user input.

Here is a simple sample code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import SwiftUI

struct ContentView: View {
@State private var name: String = ""
@FocusState private var isNameFocused: Bool

var body: some View {
VStack {
TextField("Name", text: $name)
.focused($isNameFocused)

Button("Submit") {
isNameFocused = false
// Execute the commit operation
print("Submitted: \(name)")
}
}
}
}

In this example, we have used the @State property wrapper in the ContentView to store the value of the name. We also use the @FocusState property wrapper to manage the focus state of the name text field.

By using the .focused($isNameFocused) modifier on the TextField, we associate the focus state with the name text field. This means that when the value of isNameFocused is true, the name text field will get focus.

In the action closure of the submit button, we set the value of isNameFocused to false to defocus the name text field. You can then perform commit operations or other focus-related operations.

6. @AppStorage

In SwiftUI, @AppStorage is used to conveniently read and write values in the app’s persistent storage. It provides an easy way to handle application user settings, preferences, or other data that needs to be stored persistently.

The main function of @AppStorage is to associate properties with the persistent storage of the application, so that the value of the property can be loaded when the application starts, and automatically persisted when it is changed. It uses UserDefaults to achieve data persistence.

Here is a simple sample code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import SwiftUI

struct ContentView: View {
@AppStorage("username") private var username: String = "Guest"

var body: some View {
VStack {
Text("Welcome, \(username)!")
.font(.largeTitle)

Button("Logout") {
// Clear the username and reset to default
username = "Guest"
}
}
}
}

In this example, we’ve used the @AppStorage property wrapper to associate the username property with the key “username” in the application’s persistent storage. If a value for the “username” key has been saved at application startup, then that value will be loaded into the username property.

In the view, we can directly use the username property to display the welcome message. When the “Logout” button is clicked, we can update the username by setting the username property to the new value and persist it to the application’s persistent storage.

By using the @AppStorage property wrapper, we can easily read and write to the application’s persistent storage for user settings, preferences, or other data that needs to be stored persistently. It provides a convenient way to handle persistent data for applications.

Reference

[1] https://juejin.cn/post/6976448420722507784
[2] https://zhuanlan.zhihu.com/p/151286558
[3] https://zhuanlan.zhihu.com/p/349079593
[4] https://onevcat.com/2020/06/stateobject