Difference Between Observedobject and Stateobject in Swiftui

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?

@StateObject vs @ObservedObject when passed externally but owned by the view

This is a really interesting question. There's some subtle behavior going on here.

First, notice that you can't just change @ObservedObject to @StateObject in NameView. It won't compile:

struct NameView: View {
@StateObject var viewModel: ViewModel

init(_ viewModel: ViewModel) {
self.viewModel = viewModel
// ^ Cannot assign to property: 'viewModel' is a get-only property
}
...
}

To make it compile, you have to initialize the underlying _viewModel stored property of type StateObject<ViewModel>:

struct NameView: View {
@StateObject var viewModel: ViewModel

init(_ viewModel: ViewModel) {
_viewModel = .init(wrappedValue: viewModel)
}
...
}

But there's something hidden there. StateObject.init(wrappedValue:) is declared like this:

public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)

So the expression given as an argument (just viewModel above) is wrapped up in a closure, and is not evaluated right away. That closure is stored for later use, which is why it is @escaping.

As you might guess from the hoops we have to jump through to make it compile, this is a weird way to use StateObject. The normal use looks like this:

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

var body: some View {
Text(viewModel.name)
}
}

And doing it the weird way has some drawbacks. To understand the drawbacks, we need to look at the context in which makeView() or NormalView() is evaluated. Let's say it looks like this:

struct ContentView: View {
@Binding var count: Int

var body: some View {
VStack {
Text("count: \(count)")
NormalView()
ViewFactory().makeView()
}
}
}

When count's value changes, SwiftUI will ask ContentView for its body again, which will evaluate both NormalView() and makeView() again.

So body calls NormalView() during this second evaluation, which creates another instance of NormalView. NormalView.init creates a closure which calls ViewModel(), and passes the closure to StateObject.init(wrappedValue:). But StateObject.init does not evaluate this closure immediately. It stores it away for later use.

Then body calls makeView(), which does call ViewModel() immediately. It passes the new ViewModel to NameView.init, which wraps the new ViewModel in a closure and passes the closure to StateObject.init(wrappedValue:). This StateObject also doesn't evaluate the closure immediately, but the new ViewModel has been created regardless.

Some time after ContentView.body returns, SwiftUI wants to call NormalView.body. But before doing so, it has to make sure the StateObject in this NormalView has a ViewModel. It notices that this NormalView is replacing a prior NormalView at the same position in the view hierarchy, so it retrieves the ViewModel used by that prior NormalView and puts it in the StateObject of the new NormalView. It does not execute the closure given to StateObject.init, so it does not create a new ViewModel.

Even later, SwiftUI wants to call NameView.body. But before doing so, it has to make sure the StateObject in this NameView has a ViewModel. It notices that this NameView is replacing a prior NameView at the same position in the view hierarchy, so it retrieves the ViewModel used by that prior NameView and puts it in the StateObject of the new NameView. It does not execute the closure given to StateObject.init, and so it does not use the ViewModel referenced by that closure. But the ViewModel was created anyway.

So there are two drawbacks to the weird way in which you're using @StateObject:

  1. You're creating a new ViewModel each time you call makeView, even though that ViewModel may never be used. This may be expensive, depending on your ViewModel.
  2. You're creating the ViewModel while the ContentView.body getter is running. If creating the ViewModel has side effects, this may confuse SwiftUI. SwiftUI expects the body getter to be a pure function. In the NormalView case, SwiftUI is calling the StateObject's closure at a known time when it may be better prepared to handle side effects.

So, back to your original question:

Should it be @StateObject or @ObservedObject?

Well, ha ha, that's difficult to answer without seeing an example that's less of a toy. But if you do need to use @StateObject, you should probably try to initialize it in the ‘normal’ way.

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

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).

What's the difference between @State and @StateObject

@State is a variable that is meant to hold value types such as Bools, Integers, Strings, structs and so on. Apple simply doesn't intend for @State to be used on reference types such as ObservableObjects, because once again, State is meant to store value types such as Int, not instances of classes. Apple says in the documentation,

Don’t use state properties for persistent storage because the life cycle of state variables mirrors the view life cycle. Instead, use them to manage transient state that only affects the user interface, like the highlight state of a button, filter settings, or the currently selected list item.

In short, you could use @State variables to store, say, the number of times a user has clicked a button (since the app started), but not to store another ObservableObject. Also, if you look at this, the article shows that using an Object with State will not cause the view's actual variable to update. This is because, again to quote the article,

Because we’re using a complex, reference type, the value of state itself never changes. While a property of state , num has changed, the @State property wrapper has no idea because it is only watching the variable state, not any of its properties. To SwiftUI, because it is only watching state, it has no idea that num has changed, and so never re-renders the view.

An @StateObject, on the other hand, can store things such as ObservableObjects. When the value of the Object changes, it will cause a view update, because all of it is observed by SwiftUI. This will only happen with @Published properties, though, and any change of those properties will, once again, cause the view to re-render. An important note, too: @StateObject will create a new instance every time the view appears. If you need to persist the values, you would need to pass in the object from a more root view. Also, an @StateObject can be changed from outside the view, whereas @State variables are only intended to be private and local. For more information on why this is, refer to the Apple State documentation: https://developer.apple.com/documentation/swiftui/state

Resources:

  1. https://story.tomasen.org/swiftui-difference-of-state-binding-environment-and-environmentobject-and-when-to-use-them-ff80699f45b7
  2. https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-stateobject-property-wrapper
  3. https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject
  4. https://levelup.gitconnected.com/state-vs-stateobject-vs-observedobject-vs-environmentobject-in-swiftui-81e2913d63f9#:~:text=Use%20%40%20State%20for%20very%20simple,than%20what%20%40State%20can%20handle.

What is the difference between @EnvironmentObject and @ObservedObject?

As you've noticed an @ObservedObject needs to be passed from view to view. It may be better for a simple view hierarchy when you don't have too many views.


Let's assume you have the following hierarchy:

ViewA -> ViewB -> ViewC -> ViewD

Now if you want your @ObservedObject from the ViewA to be in the ViewB there's no problem with passing it directly in init.

But what if you want it in the ViewD as well? And what if you don't need it in the ViewB and ViewC?

With an @ObservedObject you'd need to manually pass it from the ViewA to the ViewB and then to the ViewC, and then to the ViewD. And you'd need to declare it in every child view.

With an @EnvironmentObject it's easy - just pass it to the top-level view:

ViewA().environmentObject(someObservableObject)

Then you only declare it in the view that uses it - this may make your code more readable.


Note

Every object in the environment (view hierarchy) can access the injected @EnvironmentObject. If you don't want this (privacy is important) you may need to pass it as an @ObservedObject instead.

What is the difference between @State and @ObservedObject, can they both be used to persist state?

@ObservedObject does not persist state

Can I use @ObservedObject to create persisted state?

On its own, you cannot. The Apple documentation has this to say about @State:

A persistent value of a given type, through which a view reads and monitors the value.

But I found no mention of persistence with @ObservedObject so I constructed this little demo which confirms that @ObservedObject does not persist state:

class Bar: ObservableObject {
@Published var value: Int

init(bar: Int) {
self.value = bar
}
}

struct ChildView: View {
let value: Int
@ObservedObject var bar: Bar = Bar(bar: 0)

var body: some View {
VStack(alignment: .trailing) {
Text("param value: \(value)")
Text("@ObservedObject bar: \(bar.value)")
Button("(child) bar.value++") {
self.bar.value += 1
}
}
}
}

struct ContentView: View {
@State var value = 0

var body: some View {
VStack {
Spacer()
Button("(parent) value++") {
self.value += 1
}
ChildView(value: value)
Spacer()
}
}
}

Whenever you click on the value++ button, it results in a re-render of ChildView because the value property changed. When a view is re-rendered as a result of a property change, it's @ObservedObjects are reset

screenshot

In contrast, if you add a @State variable to the ChildView you'll notice that it's value is not reset when the @ObservedObject is reset.

Using persisted state with @ObservedObject

To persist state with @ObservedObject, instantiate the concrete ObservableObject with @State in the parent view. So to fix the previous example, would go like this:

struct ChildView: View {
let value: Int
@ObservedObject var bar: Bar // <-- passed in by parent view

var body: some View {
VStack(alignment: .trailing) {
Text("param value: \(value)")
Text("@ObservedObject bar: \(bar.value)")
Button("(child) bar.value++") {
self.bar.value += 1
}
}
}
}

struct ContentView: View {
@State var value = 0
@State var bar = Bar(bar: 0) // <-- The ObservableObject

var body: some View {
VStack {
Spacer()
Button("(parent) value++") {
self.value += 1
}
ChildView(value: value, bar: bar).id(1)
Spacer()
}
}
}

The definition of the class Bar is unchanged from the first code example. And now we see that the value is not reset even when the value property changes:

screen shot

How do I share/bind @StateObject between SwiftUI views?

Inject it as environment object, like

@StateObject var state = State()

var body: some View {
VStack(spacing: Constants.spacing) {
// ... other code
}
.background(...)
.clipShape(...)
.environmentObject(state) // << here !!

and use it inside children:

struct PaletteBar: View {

// MARK: - Properties

@EnvironmentObject var state: State // << injected by parent !!

// ...
}

@State vs @ObservableObject - which and when?

If you mark any variables as @State in a SwiftUI View and bind them to a property inside the body of that View, the body will be recalculated whenever the @State variable changes and hence your whole View will be redrawn. Also, @State variables should serve as the single source of truth for a View. For these reasons, @State variables should only be accessed and updated from within the body of a View and hence should be declared private.

You should use @State when you are binding some user input (such as the value of a TextField or the chosen value from a Picker). @State should be used for value types (structs and enums).

On the other hand, @ObservedObject should be used for reference types (classes), since they trigger refreshing a view whenever any @Published property of the ObservableObject changes.

You should use @ObservedObject when you have some data coming in from outside your View, such as in an MVVM architecture with SwiftUI, your ViewModel should be stored as an @ObservedObject on your View.

A common mistake with @ObservedObjects is to declare and initialise them inside the View itself. This will lead to problems, since every time the @ObservedObject emits an update (one of its @Published properties gets updated), the view will be recreated - which will also create a new @ObservedObject, since it was initialised in the View itself. To avoid this problem, whenever you use @ObservedObject, you always have to inject it into the view. The iOS 14 @StateObject solves this issue.



Related Topics



Leave a reply



Submit