Initialize @StateObject with a parameter in SwiftUI

Short Answer

The StateObject has the next init: init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType). This means that the StateObject will create an instance of the object at the right time - before running body for the first time. But it doesn't mean that you must declare that instance in one line in a View like @StateObject var viewModel = ContentViewModel().

The solution I found is to pass a closure as well and allow StateObject to create an instance on an object. This solution works well. For more details read the Long Answer below.

class ContentViewModel: ObservableObject {}

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: @autoclosure @escaping () -> ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel())
    }
}

struct RootView: View {
    var body: some View {
        ContentView(viewModel: ContentViewModel())
    }
}

No matter how many times RootView will create its body, the instance of ContentViewModel will be only one.

In this way, you are able to initialize @StateObject view model which has a parameter.

Long Answer

@StateObject

The @StateObject creates an instance of value just before running body for the first time (Data Essentials in SwiftUI). And it keeps this one instance of the value during all view lifetime. You can create an instance of a view somewhere outside of a body and you will see that init of ContentViewModel will not be called. See onAppear in the example below:

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

struct RootView: View {
    var body: some View {
        VStack(spacing: 20) {
        //...
        }
        .onAppear {
            // Instances of ContentViewModel will not be initialized
            _ = ContentView()
            _ = ContentView()
            _ = ContentView()

            // The next line of code
            // will create an instance of ContentViewModel.
            // Buy don't call body on your own in projects :)
            _ = ContentView().view
        }
    }
}

Therefore it's important to delegate creating an instance to StateObject.

Why should not use StateObject(wrappedValue:) with instance

Let's consider an example when we create an instance of StateObject with _viewModel = StateObject(wrappedValue: viewModel) by passing a viewModel instance. When the root view will trigger an additional call of the body, then the new instance on viewModel will be created. If your view is an entire screen view, that will probably work fine. Despite this fact better not to use this solution. Because you're never sure when and how the parent view redrawing its children.

final class ContentViewModel: ObservableObject {
    @Published var text = "Hello @StateObject"
    
    init() { print("ViewModel init") }
    deinit { print("ViewModel deinit") }
}

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel)
        print("ContentView init")
    }

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

struct RootView: View {
    @State var isOn = false
    
    var body: some View {
        VStack(spacing: 20) {
            ContentView(viewModel: ContentViewModel())
            
            // This code is change the hierarchy of the root view.
            // Therefore all child views are created completely,
            // including 'ContentView'
            if isOn { Text("is on") }
            
            Button("Trigger") { isOn.toggle() }
        }
    }
}

I tapped "Trigger" button 3 times and this is the output in the Xcode console:

ViewModel init
ContentView init
ViewModel init
ContentView init
ViewModel init
ContentView init
ViewModel deinit
ViewModel init
ContentView init
ViewModel deinit

As you can see, the instance of the ContentViewModel was created many times. That's because when a root view hierarchy is changed then everything in its body is created from scratch, including ContentViewModel. No matter that you set it to @StateObject in the child view. The matter that you call init in the root view the same amount of times as how the root view made an update of the body.

Using closure

As far as the StateObject use closure in the init - init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) we can use this and pass the closure as well. Code exactly the same with previous section (ContentViewModel and RootView) but the only difference is using closure as init parameter to the ContentView:

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: @autoclosure @escaping () -> ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel())
        print("ContentView init")
    }

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

After "Trigger" button was tapped 3 times - the output is next:

ContentView init
ViewModel init
ContentView init
ContentView init
ContentView init

You can see that only one instance of ContentViewModel has been created. Also the ContentViewModel was created after ContentView.

Btw, the easiest way to do the same is to have the property as internal/public and remove init:

struct ContentView: View {
    @StateObject var viewModel: ContentViewModel
}

The result is the same. But the viewModel can not be private property in this case.


Here is a demo of solution. Tested with Xcode 12b.

class MyObject: ObservableObject {
    @Published var id: Int
    init(id: Int) {
        self.id = id
    }
}

struct MyView: View {
    @StateObject private var object: MyObject
    init(id: Int = 1) {
        _object = StateObject(wrappedValue: MyObject(id: id))
    }

    var body: some View {
        Text("Test: \(object.id)")
    }
}

The answer given by @Asperi should be avoided Apple says so in their documentation for StateObject.

You don’t call this initializer directly. Instead, declare a property with the @StateObject attribute in a View, App, or Scene, and provide an initial value.

Apple tries to optimize a lot under the hood, don't fight the system.

Just create an ObservableObject with a Published value for the parameter you wanted to use in the first place. Then use the .onAppear() to set it's value and SwiftUI will do the rest.

Code:

class SampleObject: ObservableObject {
    @Published var id: Int = 0
}

struct MainView: View {
    @StateObject private var sampleObject = SampleObject()
    
    var body: some View {
        Text("Identifier: \(sampleObject.id)")
            .onAppear() {
                sampleObject.id = 9000
            }
    }
}