What is the difference between ObservedObject and StateObject in SwiftUI

@ObservedObject

When a view creates its own @ObservedObject instance it is recreated every time a view is discarded and redrawn:

struct ContentView: View {
  @ObservedObject var viewModel = ViewModel()
}

On the contrary a @State variable will keep its value when a view is redrawn.

@StateObject

A @StateObject is a combination of @ObservedObject and @State - the instance of the ViewModel will be kept and reused even after a view is discarded and redrawn:

struct ContentView: View {
  @StateObject var viewModel = ViewModel()
}

Performance

Although an @ObservedObject can impact the performance if the View is forced to recreate a heavy-weight object often, it should not matter much when the @ObservedObject is not complex.

When to use @ObservedObject

It might appear there is no reason now to use an @ObservedObject, so when should it be used?

You should use @StateObject for any observable properties that you initialize in the view that uses it. If the ObservableObject instance is created externally and passed to the view that uses it mark your property with @ObservedObject.

Note there are too many use-cases possible and sometimes it may be desired to recreate an observable property in your View. In that case it's better to use an @ObservedObject.

Useful links:

  • What’s the difference between @StateObject and @ObservedObject?
  • What’s the difference between @ObservedObject, @State, and @EnvironmentObject?
  • What is the @StateObject property wrapper?

Even though pawello2222's answer have nicely explained the differences when the view itself creates its view model, it's important to note the differences when the view model is injected into the view.

When you inject the view model into the view, as long as the view model is a reference type, there are no differences between @ObservedObject and @StateObject, since the object that injected the view model into your view should hold a reference to view model as well, hence the view model isn't destroyed when the child view is redrawn.

class ViewModel: ObservableObject {}

struct ParentView: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        ChildView(viewModel: viewModel) // You inject the view model into the child view
    }
}

// Even if `ChildView` is discarded/redrawn, `ViewModel` is kept in memory, since `ParentView` still holds a reference to it - `ViewModel` is only released and hence destroyed when `ParentView` is destroyed/redrawn.
struct ChildView: View {
    @ObservedObject var viewModel: ViewModel
}

Apple documentation did explain why initializing with ObservedObject is unsafe.

SwiftUI might create or recreate a view at any time, so it’s important that initializing a view with a given set of inputs always results in the same view. As a result, it’s unsafe to create an observed object inside a view.

The solution is StateObject.

At the same time, the documentation showed us how we should create data models in a view (or app/scene) when it can hold on to the truth, and pass it to another view.

struct LibraryView: View {
    @StateObject var book = Book() // Hold on to the 1 truth
    var body: some View {
        BookView(book: book) // Pass it to another view
    }
}

struct BookView: View {
    @ObservedObject var book: Book // From external source
}

Here is an example to illustrate the difference.

Every time you click the Refresh button the StateObjectClass is recreated from scratch only for CountViewObserved. This means it's @Published count property gets the default value of 0 when this happens.

The difference between @StateObject and @ObservedObject is clear. The @StateObject version of the observed StateObjectClass preserves its state since it is never deinitted. The @ObservedObject version does not as it is recreated. So you should use @StateObject for the owner of an ObservableObject.

import SwiftUI

class StateObjectClass: ObservableObject {
    enum ObserverType: String {
        case stateObject
        case observedObject
    }
    
    @Published var count = 0
    let type: ObserverType
    let id = UUID()
    init(type: ObserverType) {
        self.type = type
    }
    deinit {
        print(#function, "type: \(type.rawValue) id: \(id)")
    }
}

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

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

struct ContentView: View {
    @State private var count = 0
    var body: some View {
        VStack {

            Text("Refresh CounterView's count: \(count)")
            
            Button("Refresh") {
                count += 1
            }

            CountViewState()
                .padding()

            CountViewObserved()
                .padding()

        }
    }
}

Tags:

Ios

Swift

Swiftui