Search code examples
swiftswiftuicombineobservedobject

What is the difference between @StateObject and @ObservedObject in child views in swiftUI


I created a Model like this:

class TestModel: ObservableObject {
    @Published var num: Int = 0
}

Model is be used in "Home" view and "Home"s child view "HomeSub"

struct Home: View {
    
    @StateObject var model = TestModel()
    
    var body: some View {
        NavigationView(content: {
            NavigationLink(destination: HomeSub(model: model)) { Text("\(model.num)") }
        })
    }
}
struct HomeSub: View {
   //1
    @StateObject var model = TestModel()
   //2
    @ObservedObject var model = TestModel()

    var body: some View {
        VStack {
            Text("\(model.num)")
                .padding()
                .background(Color.red)
            Button("Add") {
                model.num += 1
            }
        }
        .onChange(of: model.num, perform: { value in
            print("homeSub: \(value)")
        })
        
    }
}

In HomeSub view, what is the difference between 1 and 2? When I run the project, they have exactly the same behavior.


Solution

  • As you've written it, both @StateObject and @ObservedObject are doing the same thing in the child view. But, neither is correct because they are unnecessarily creating a new TestModel just to toss it and replace it with the one being passed in.

    The correct way to write the child view is:

    @ObservedObject var model: TestModel
    

    In this case, no initial value is assigned to model in the child view, which means the caller will have to provide it. This is exactly what you want. One source of truth which is the model in the parent view.

    Also, state variables (both @State and @StateObject) should be private to a view and should always be marked with private. If you had done this:

    @StateObject private var model = TestModel()
    

    in the child view, then you wouldn't have been able to pass the model from the parent view and you would have seen that only @ObservedObject can be used in this case.


    Upon further testing, it seems that Swift/SwiftUI avoids creating the TestModel in the child view when it is written as @ObservedObject var model = TestModel(), but that syntax is still misleading to the reader and it should still be written as @ObservedObject var model: TestModel because that makes it clear that model is being initialized from somewhere else (that is, from the parent view).