SwiftUI 2.0 —— Research on @StateObject

Published on

Get weekly handpicked updates on Swift and SwiftUI!

WWDC 20 just concluded, and in the past week, Apple has brought huge surprises to developers. Due to the plethora of new features, it takes some time to digest them. I first chose the content I am most interested in for some simple research and discussion. This article first briefly discusses the new property wrapper @StateObject provided by SwiftUI.

Why Introduce @StateObject

In my previous article @State Research in SwiftUI, we discussed @State, which allows us to conveniently use value-type data as the Source of truth for a View. In the SwiftUI 1.0 era, if you wanted to use a reference type as the source of truth, the common method was to use @EnvironmentObject or @ObservedObject.

Swift
struct RootView:View{
    var body: some View{
        ContentView()
            .environmentObject(Store())
    }
}

struct ContentView: View {
    @EnvironmentObject  var store1:Store
    var body: some View {
        Text("count:\(store.count)")
    }
}

For data injected using @EnvironmentObject, since it is usually created in the SceneDelegate or the parent or ancestor View of the current View, its lifecycle is necessarily not shorter than the current View. Therefore, there are no unexpected exceptions due to unpredictable lifecycles in use.

Swift
struct Test5: View {
    @ObservedObject var store = Store()
    var body: some View {
        Text("count:\(store.count)")
    }
}

At first glance, the above code seems unproblematic, but due to the mechanism of @ObservedObject, the instance it creates is not owned by the current View (the current View cannot manage its lifecycle), so in some special cases, unpredictable results may occur.

To give developers better control over their code while maintaining good compatibility with the previous version, Apple added @StateObject in SwiftUI 2.0. As the name suggests, it is the reference type version of @State.

In the WWDC video, Apple explicitly stated that @StateObject is owned by the View that created it, meaning the lifecycle of the instance is completely controllable and the same as the lifecycle of the View that created it.

The difference between @StateObject and @ObservedObject is whether the instance is owned by the View that created it and whether its lifecycle is completely controllable.

Understanding the Differences Through Code

I explain the differences between @StateObject and @ObservedObject in detail through the following code.

Preparation:

Swift
class StateObjectClass:ObservableObject{
    let type:String
    let id:Int
    @Published var count = 0
    init(type:String){
        self.type = type
        self.id = Int.random(in: 0...1000)
        print("type:\(type) id:\(id) init")
    }
    deinit {
        print("type:\(type) id:\(id) deinit")
    }
}

struct CountViewState:View{
    @StateObject var state = StateObjectClass(type:"StateObject")
    var body: some View{
        VStack{
            Text("@StateObject count :\(state.count)")
            Button("+1"){
                state.count += 1
            }
        }
    }
}

struct CountViewObserved:View{
    @ObservedObject var state = StateObjectClass(type:"Observed")
    var body: some View{
        VStack{
            Text("@Observed count :\(state.count)")
            Button("+1"){
                state.count += 1
            }
        }
    }
}

StateObjectClass will inform us of the method it was created by and which specific instance was destroyed when it is created and destroyed.

The only difference between CountViewState and CountViewObserved is the property wrapper used to create the instance.

Test 1:

Swift
struct Test1: View {
    @State var count = 0
    var body: some View {
        VStack{
            Text("Refresh CounterView Count :\(count)")
            Button("Refresh"){
                count += 1
            }
            
            CountViewState()
                .padding()
            
            CountViewObserved()
                .padding()
            
        }
    }
}

In Test 1, when the +1 button is clicked, both @StateObject and @ObservedObject show consistent behavior, and both Views can normally display the current number of button clicks. However, when the Refresh button is clicked, the number in CountViewState remains normal, but the count in CountViewObserved is reset to zero. From the debug information, we can see that when Refresh is clicked, the instance in CountViewObserved is recreated and the previous instance is destroyed (the CountViewObserved view is not

recreated, only the body value is recalculated).

Swift
type:Observed id:443 init
type:Observed id:103 deinit

In this test, the instance created by @ObservedObject has a shorter lifecycle than the current View.

Test 2:

Swift
struct Test2: View {
    @State var count = 0
    var body: some View {
        NavigationView{
            List{
                NavigationLink("@StateObject", destination: CountViewState())
                NavigationLink("@ObservedObject", destination: CountViewObserved())
            }
        }
    }
}

In Test 2, after clicking the link to enter the corresponding View and clicking +1 to count, then returning to the parent view. When re-entering the link, the count in the @StateObject corresponding view is reset (since the view is recreated when returning to the parent view and re-entered, the instance is recreated), but the count in the @ObservedObject corresponding view is not reset.

In this test, the instance created by @ObservedObject has a longer lifecycle than the current View.

Test 3:

Swift
struct Test3: View {
    @State private var showStateObjectSheet = false
    @State private var showObservedObjectSheet = false
    var body: some View {
        List{
            Button("Show StateObject Sheet"){
                showStateObjectSheet.toggle()
            }
            .sheet(isPresented: $showStateObjectSheet) {
                CountViewState()
            }
            Button("Show ObservedObject Sheet"){
                showObservedObjectSheet.toggle()
            }
            .sheet(isPresented: $showObservedObjectSheet) {
                CountViewObserved()
            }   
        }
    }
}

In Test 3, click the button and click +1 in the sheet. When re-entering the sheet, regardless of whether it is @StateObject or @ObservedObject, the count in the corresponding View is reset.

In this test, the lifecycle of the instance created by @ObservedObject is consistent with the View.

Three pieces of code, three different results, this is why Apple introduced @StateObject - to allow developers to clearly understand and control the lifecycle of instances, eliminating uncertainty!

Is There Still a Need for ObservedObject?

Certainly, there is!

The fundamental reason why a StateObject can create a stable lifecycle for its instance is due to the uniqueness it brings to the instance: an object annotated with @StateObject remains unique throughout the entire lifecycle of the view. This means that even if the view is re-rendered, the object is not recreated.

However, this also means that in some scenarios where we do not need this uniqueness, @ObservedObject is the correct choice.

For more details, please read the section “When to Choose to Use ObservedObject” in StateObject and ObservedObject.

Next

With the introduction of @StateObject, Apple has not only fixed previous vulnerabilities but also, through the introduction of numerous new features in SwiftUI 2.0, further perfected the implementation methods of Data Flow.

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed