Small knowledge, big challenge! This article is participating in the “Essentials for Programmers” creative activity. This article has participated in the “Digitalstar Project” to win a creative gift package and challenge the creative incentive money

The preface

@observedobject regardless of the store, will be created multiple times as the View is created. The @stateObject guarantees that the object will only be created once. Therefore, if you are creating an ObservableObject Model object yourself in the View, @stateObject is probably the more correct choice. @stateObject is basically a @state upgrade for class.

State Management for the first SwiftUI

At the time of SwiftUI’s launch in 2019, there were three State management-related property wrappers, @State, in addition to @gestureState, which was used to manage gestures. @ ObservedObject and @ EnvironmentObject. Depending on the responsibilities and scope of action, each of these applies to different scenarios. In general:

  • @state is used for private State values in a View. Generally, all that it decorates should be struct values and should not be seen by other views. It represents the least scoped and simplest state in SwiftUI, such as a Bool, Int, or String. In short, if a State can be marked private and it is a value type, then @state is appropriate.

  • For a more complex set of states, we can organize it in a class that implements the ObservableObject protocol. For such a class type, a property marked @published will automatically issue an event notifying the View on which it depends to update when it changes. If the View needs to rely on such an ObservableObject, it is subscribed to using @obServedobject when it is declared.

  • EnvironmentObject For an ObservableObject that needs to be passed to a deeper child View, we can use the. EnvironmentObject modifier on the parent View to inject it into the environment. Any child View can then retrieve the object via @environmentobject.

This is basically all there is to the first version of SwiftUI state Management

It looks like SwiftUI is pretty well covered for state management, so why add a @StateObject Property wrapper? To figure this out, let’s first look at the problem with @observedobject.

What’s wrong with @observedobject

Let’s consider implementing an interface like this:

When you click “Toggle Name”, the Current User switches between the real Name and nickname. Click “+1” to renew the View for one second unconditionally

The Score displayed increases by 1.

Take a look at the following code, which is less than 50 lines long:

**struct** ObjectView: View { @State **private** **var** showRealName = **false** **var** body: **some** View { VStack { Button("Toggle Name") { showRealName.toggle() } Text("Current User: \(showRealName ? "Wei Wang" : "onevcat")") ScorePlate().padding(.top, 20) } } } \ **class** Model: ObservableObject { **init**() { print("Model Created") } @Published **var** score: Int = 0 } **struct** ScorePlate: View { @ObservedObject **var** model = Model() @State **private** **var** niceScore = **false** **var** body: **some** View { VStack { Button("+1") { **if** model.score > 3 { niceScore = **true** } model.score += 1 Print (" * * * * * this is what ")} Text (Score: \ "(model. Score)"), Text (" Nice? \(niceScore ? "YES" : "NO")") ScoreText(model: model).padding(.top, 20) } } } **struct** ScoreText: View { @ObservedObject **var** model: Model **var** body: **some** View { **if** model.score > 10 { **return** Text("Fantastic") } **else** **if** model.score > 3 { **return** Text("Good") } **else** { **return** Text("Ummmm..." )}}}Copy the code

A brief explanation of behavior:

For the Toggle Name button and the Current User tag, they are written directly into the ContentView. The +1 button and the part that displays the score and its status are encapsulated in a View called ScorePlate. It needs a Model to keep track of the scores, the Model. In ScorePlate, we declare it as an @observedobject variable:

struct ScorePlate: View {
    @ObservedObject var model = Model()
    //...
}
Copy the code

In addition to the Model, we have added another private Boolean State @state niceScore to ScorePlate. In addition to increasing model.score each time +1, we also check if it is greater than three and set niceScore accordingly. We can use it to look at the difference in behavior between at sign State and at sign ObservedObject.

Finally, the bottom line is another View: ScoreText. It also contains an @observedobject Model and determines the text content to display based on the score value. The model is passed in at initialization:

struct ScorePlate: View {
    var body: some View {
        // ...
        ScoreText(model: model).padding(.top, 20)
    }
}
Copy the code

Of course, in this example, a simple @state Int is enough, but to illustrate the problem, we created a Model. In real projects, the Model is more complex than an Int.

The “+1” button works perfectly when we try to run it, “Nice” and “Ummmm…” The text can also change as expected, and everything works perfectly… Until we wanted to change the Name with “Toggle Name” :

Except for the Nice tag (driven by @state), the rest of the ScorePlate text has been reset by a seemingly unrelated operation! This is clearly not the behavior we want.

(In order to save traffic and respect BLM, please imagine the question mark of African americans here)

This is because, unlike @State, where the underlying storage is “taken over” by SwiftUI, @Observedobject simply adds a subscription between the View and Model without affecting the storage. Therefore, when the state in the ContentView changes and contentView.body is reevaluated, the ScorePlate is regenerated, along with the model, resulting in a “loss” of state. Run the code, and you can see in the Xcode Console that every time you click the Toggle button you get the output of Model.init.

The Nice tag, on the other hand, is driven by @state: Since the View is an immutable struct, its State changes require support from the underlying storage. SwiftUI will create extra storage for @State to ensure that State is maintained when the View is refreshed (i.e. recreated). But that doesn’t work for @observedobject.

Ensure that the @stateObject is created once

Once you understand the problems with @observedobject, the @StateObject makes sense. @StateObject is an updated version of @State: @state is the store created for struct State, and @StateObject is the store for ObservableObjectclass. It guarantees that the class instance will not be recreated along with the View. To solve the problem.

In the concrete example above, just change the @obServedobject in ScorePlate to @StateObject and you’ll be fine:

struct ScorePlate: View {
    // @ObservedObject var model = Model()
    @StateObject var model = Model()
}
Copy the code

The state in ScorePlate and ScoreText will no longer be reset.

So, the natural question to ask is, should we replace all @observedobobjects with @stateObject? For example in the example above do I need to replace the declaration in ScoreText as well? It depends on how your View is actually expected to behave: If you don’t want the Model state to be lost when the View is refreshed, you can definitely replace it, and this (although it may have some performance impact) won’t affect the overall behavior. However, if the View itself expects a new state every time it refreshes, then @stateObject is not appropriate for observableObjects that are not created themselves, but are received from the outside.

More discussion

Use @environmentobject to keep up

In addition to @stateobject, another way to keep a StateObject alive is to use.environmentobject on the outer layer:

struct SwiftUINewApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(Model())
        }
    }
}
Copy the code

This way, the Model object will be injected into the environment and will not change as the ContentView is refreshed. To use, just follow the normal environment approach and declare Model as @environmentobject:

struct ScorePlate: View {
    @EnvironmentObject var model: Model
    // ...
    
    // ScoreText(model: model).padding(.top, 20)
    ScoreText().padding(.top, 20)
}

struct ScoreText: View {
    @EnvironmentObject var model: Model
    // ...
}
Copy the code

Keep the same life cycle as @state

In addition to ensuring a single creation, another important feature of @StateObject is to be consistent with @State’s “life cycle”, allowing SwiftUI to take over the storage behind it and avoiding unnecessary bugs.

Make a few changes to the ContentView and put ScorePlate() in a NavigationLink to see the result:

var body: some View {
  NavigationView {
    VStack {
      Button("Toggle Name") {
        showRealName.toggle()
      }
      Text("Current User: (showRealName ? "Wei Wang" : "onevcat")")
      NavigationLink("Next", destination: ScorePlate().padding(.top, 20))
    }
  }
}
Copy the code

When you click “Next,” you navigate to the ScorePlate page, where you can do the +1 action. When you click the Back button to return to the ContentView and click “Next” again, you would normally expect the ScorePlate state to be reset to a new, zero-based state. Using @StateObject works fine at this point because SwiftUI helped us rebuild @State and @StateObject. If we changed the declaration in ScorePlate from @StateObject back to @observedobject, SwiftUI would no longer be able to help us manage our state unless we refreshed the entire ContentView with the “Toggle” button, Otherwise, the ScorePlate will remain in its original state when displayed again.

Of course, if you want to preserve these states in ScorePlate, using @observedobject or @environmentobject above is the way to go.

conclusion

In short, for ObservableObject state objects created by the View itself, there is a high probability that you will need to use the new @StateObject to make its storage and lifecycle more reasonable:

struct MyView: View {
    @StateObject var model = Model()
}
Copy the code

For views that receive an ObservableObject from the outside, whether to use @obServedobject or @StateObject depends on the situation and needs. Views, such as those in NavigationLink destinations, need to be handled with extra care since SwiftUI doesn’t do lazy construction on them.

In either case, thoroughly understanding the difference and the logic behind it can help us better understand the behavior of a SwiftUI app.