Explanation behind the error? "Accessing StateObject's object without being installed on a View. This will create a new instance each time."

I was reading this Apple Dev article – Managing Model Data in Your App, and I thought in places where I was using @ObservedObject I should really be using @StateObject. As, I thought View should own and manage some of these simple objects. So, I gave that a go and I started getting the following runtime error in Xcode:

"Accessing StateObject's object without being installed on a View. This will create a new instance each time."

It is not clear to me what it means. And the web didn't turn up anything useful. It seems I'm misunderstanding how StateObject works. I created a small sample project and reproduced the error. It's using the standard UIKit AppDelegate approach with a SwiftUI view. Here are the relevant code bits:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    let contentView = ContentView()

    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: contentView)
        self.window = window
        window.makeKeyAndVisible()
    }

    contentView.model.message = "bye" // This is where Xcode highlights with that runtime error.
}

.....

}

class Model: ObservableObject {
    var message: String

    init(m: String) {
        message = m
    }
}

struct ContentView: View {
    @StateObject var model = Model(m: "Hello")

    var body: some View {
        Text(model.message)
    }
}

The program just displays "Hello" when it's run. But I would have expected it to display "bye".

Can anyone explain to me what is wrong with the code and the explanation behind the runtime error?

2 Likes

In SwiftUI, you can think of the View struct not as a persistent object in and of itself, but as a description of a View. Thus, when body is called, it creates a new description of the view’s contents, which are applied to the persistent View that’s kept alive by the system.

@StateObject is special — it’s more like a reference to an object that’s persisted just like the View itself, and tied to the persistent View’s lifetime. Thus, when you create an instance of a View and then immediately access the @StateObject, you’re not accessing the persistent backing object, you’re accessing a dummy placeholder that hasn’t been installed into the persistent View hierarchy yet.

The way you work within the system here is to change the value in a .onAppear {...} modifier on your View. This will get called after the View has been installed into the hierarchy, and the system will have filled in the appropriate backing store for the StateObject on your behalf.

5 Likes

Here’s an article that explains the internals of this some more.

4 Likes

Thanks for the explanation. Is .onAppear {} the only way to safely mutate the backing object (i.e model in this case)? In my code example, I'm trying to mutate it from the SceneDelegate class. This may not be the best coding example, but it's close to a hybrid app where I might be bringing SwiftUI stuff to UIKit and trying to get them to talk to each other.

For example, if I have a VC which created a SwiftUI View which in turn creates a @StateObject object. And then I might want to pass some info from the VC to that object. What's the recommended approach here?

If you're creating an object that you intend to be edited outside of SwiftUI, then the object shouldn't be an @StateObject -- @StateObject is meant for models that are owned by the SwiftUI view hierarchy. What you likely want is to create your object outside the View and pass it in either as an @ObservedObject or as an @EnvironmentObject via .environmentObject.

This goes back to the core concept of SwiftUI's state management: there should only be one source of truth. If you have something that UIKit needs, and UIKit is hosting the SwiftUI, then UIKit should be the source of truth here and the object should be given to SwiftUI to observe.

10 Likes

Perfect, I think it just clicked for me. Thanks for the clear explanation.

1 Like

That contradicts the Apple documentation, and every example I've seen. Apple has an example here that initializes a StateObject at declaration, and uses it in the view without onAppear or anything else.

Yes, the documented recommendations have changed to explicitly call out one way to do this kind of injection that is safe. This recommendation was added several years after my post :sweat_smile:

Not shocking. SwiftUI (and its underlying observation framework) really weren't well thought-out.