[SwiftUI] We can't inject @StateObject since it's read-only

@StateObject is read-only, so we can't inject it.

The next block of code shows how we could inject 2 presenters through the init, which are then assigned to a @StateObject variable and a @ObservedObject variable.

Notice:
  • Assigning a value directly to a @StateObject works fine. (presenterStateWorking).

  • Injecting a presenter that is then assigned to a @StateObject doesn't work. Throws an error in the init. (presenterStateNotWorking).

  • Injecting a presenter that is then assigned to a @ObservedObject works fine. (presenterObserved).


Code Block swift
/* (You can drop this chunk of code in a playground) */
import SwiftUI
import PlaygroundSupport
class Presenter: ObservableObject {
    @Published var viewModel = ViewModel(number: 42)
}
struct ViewModel: Equatable {
    let number: Int
}
struct ContentView: View {
    @StateObject var presenterStateWorking = Presenter()
    @StateObject var presenterStateNotWorking: Presenter
    @ObservedObject var presenterObserved: Presenter
    init(presenter1: Presenter, presenter2: Presenter) {
        self.presenterStateNotWorking = presenter1 /* (error thrown on this line) */
        self.presenterObserved = presenter2
    }
    var body: some View {
        VStack {
            Text("Hi 🍎 Engineer 😊")
                .padding()
        }
    }
}
let view = ContentView(presenter1: Presenter(), presenter2: Presenter())
PlaygroundPage.current.liveView = UIHostingController(rootView: view)


(Feedback issue number: FB7774103)
Answered by Documentation Engineer in 614929022
A @StateObject is only meant for instantiating an object at some point in your view hierarchy, as you've done with presenterStateWorking. This attribute tells SwiftUI to create the object only once, regardless of how many times it recreates the enclosing view. It isn't meant for passing an object through the interface, as you've done with presenterStateWorking. For that, use @ObservedObject, as you have with presenterObserved.

For more information about creating and passing around model objects, please see Managing Model Data in Your App.
Accepted Answer
A @StateObject is only meant for instantiating an object at some point in your view hierarchy, as you've done with presenterStateWorking. This attribute tells SwiftUI to create the object only once, regardless of how many times it recreates the enclosing view. It isn't meant for passing an object through the interface, as you've done with presenterStateWorking. For that, use @ObservedObject, as you have with presenterObserved.

For more information about creating and passing around model objects, please see Managing Model Data in Your App.
Thank you for your answer, Documentation Engineer.

I have another question to follow:
  • How do you mock @StateObject properties for testing purposes then?

If you remove the initializer you defined for ContentView, apparently there is an auto generated initializer that allows you to override your presenter.
The auto generated initializer that @KhaosT mentions allows you to inject the StateObject.

The auto generated initializer must have functionality under the hood that tells SwiftUI to create the object only once. I haven't found a way to create your own custom init function if you want to inject StateObjects.
You can inject @StateObject during initialization by unwrapping the property with _

Example:
Code Block swift
struct BinderView: View {
  @StateObject var binder: Binder
  init(name: String) {
    _binder = StateObject(wrappedValue: Binder(name: name))
  }
  var body: some View {
    Text(binder.name)
  }
}


Initializing the State Object in the init using StateObject(wrappedValue:) is not longer working for me. Maybe is beta4. I tried to look if Apple made any change, but I did not find anything.


I found the true error in the problem I was having using StateObject(wrappedValue:).

One of the params that I need to initialize the object is given by @Environment, and it seems that I can't access that var in the struct initializer.


[SwiftUI] We can't inject @StateObject since it's read-only
 
 
Q