[Shorts] Injecting @StateObject into your views

Here, we'll see how to properly inject a @StateObject data model into your view on init

At some point you might need to manually inject/create your @StateObject data model for your view. This is usually the case when the data model needs to be initialized with a set of dependencies that aren't available from within the view. Let's first see the common mistake most people make when trying to do this. After that, we'll show the correct way to achieve this.

Anti-Pattern

struct SomeView: View {
    @StateObject var viewModel: Model

    init(viewModel: Model) {
        self._viewModel = StateObject(wrappedValue: viewModel)
    }

    var body: some View { ... }
}

This is incorrect because @StateObject property wrapper is designed to hold off on initializing its wrapped value until the first invocation of the body property. Passing the viewModel directly into the init will cause the viewModel to first be initialized before it is assigned to the @StateObject's wrappedValue.

Correct

struct SomeView: View {
    @StateObject var viewModel: Model

    init(viewModel: @autoclosure @escaping Model) {
        self._viewModel = StateObject(wrappedValue: viewModel())
    }

    var body: some View { ... }
}

By marking the viewModel param with @autoclosure we are deferring the execution of the expression that will result in a Model object and instead wrapping that into a closure. That closure can then be assigned to the @StateObject's wrappedValue on init. Because the viewModel param is now a closure, we need to execute the closure when passing it into the wrappedValue by adding (). We have to do this because the wrappedValue param also uses an @autoclosure. This allows StateObject to only execute the viewModel creation closure lazily the first time the body property is invoked.

Conclusion

Hope you found this useful in some way. Thanks for reading. 🙏🏿

Find me on twitter or contact me if you have any questions or suggestions.

References

@StateObject init documentation