Initialize @StateObject with a parameter in SwiftUI

SwiftSwiftuiXcode12

Swift Problem Overview


I would like to know if there is currently (at the time of asking, the first Xcode 12.0 Beta) a way to initialize a @StateObject with a parameter coming from an initializer.

To be more specific, this snippet of code works fine:

struct MyView: View {
  @StateObject var myObject = MyObject(id: 1)
}

But this does not:

struct MyView: View {
  @StateObject var myObject: MyObject

  init(id: Int) {
    self.myObject = MyObject(id: id)
  }
}

From what I understand the role of @StateObject is to make the view the owner of the object. The current workaround I use is to pass the already initialized MyObject instance like this:

struct MyView: View {
  @ObservedObject var myObject: MyObject

  init(myObject: MyObject) {
    self.myObject = myObject
  }
}

But now, as far as I understand, the view that created the object owns it, while this view does not.

Thanks.

Swift Solutions


Solution 1 - Swift

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)")
    }
}

backup

Solution 2 - Swift

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
            }
    }
}

Solution 3 - Swift

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.

Solution 4 - Swift

Like @Mark pointed out, you should not handle @StateObject anywhere during initialization. That is because the @StateObject gets initialized after the View.init() and slightly before/after the body gets called.

I tried a lot of different approaches on how to pass data from one view to another and came up with a solution that fits for simple and complex views / view models.

Version

Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)

This solution works with iOS 14.0 upwards, because you need the .onChange() view modifier. The example is written in Swift Playgrounds. If you need an onChange like modifier for lower versions, you should write your own modifier.

Main View

The main view has a @StateObject viewModel handling all of the views logic, like the button tap and the "data" (testingID: String) -> Check the ViewModel

struct TestMainView: View {
    
    @StateObject var viewModel: ViewModel = .init()
    
    var body: some View {
        VStack {
            Button(action: { self.viewModel.didTapButton() }) {
                Text("TAP")
            }
            Spacer()
            SubView(text: $viewModel.testingID)
        }.frame(width: 300, height: 400)
    }
    
}

Main View Model (ViewModel)

The viewModel publishes a testID: String?. This testID can be any kind of object (e.g. configuration object a.s.o, you name it), for this example it is just a string also needed in the sub view.

final class ViewModel: ObservableObject {
    
    @Published var testingID: String?
    
    func didTapButton() {
        self.testingID = UUID().uuidString
    }
    
}

So by tapping the button, our ViewModel will update the testID. We also want this testID in our SubView and if it changes, we also want our SubView to recognize and handle these changes. Through the ViewModel @Published var testingID we are able to publish changes to our view. Now let's take a look at our SubView and SubViewModel.

SubView

So the SubView has its own @StateObject to handle its own logic. It is completely separated from other views and ViewModels. In this example the SubView only presents the testID from its MainView. But remember, it can be any kind of object like presets and configurations for a database request.

struct SubView: View {
    
    @StateObject var viewModel: SubviewModel = .init()
    
    @Binding var test: String?
    init(text: Binding<String?>) {
        self._test = text
    }
    
    var body: some View {
        Text(self.viewModel.subViewText ?? "no text")
            .onChange(of: self.test) { (text) in
                self.viewModel.updateText(text: text)
            }
            .onAppear(perform: { self.viewModel.updateText(text: test) })
    }
}

To "connect" our testingID published by our MainViewModel we initialize our SubView with a @Binding. So now we have the same testingID in our SubView. But we don't want to use it in the view directly, instead we need to pass the data into our SubViewModel, remember our SubViewModel is a @StateObject to handle all the logic. And we can't pass the value into our @StateObject during view initialization, like I wrote in the beginning. Also if the data (testingID: String) changes in our MainViewModel, our SubViewModel should recognize and handle these changes.

Therefore we are using two ViewModifiers.

onChange

.onChange(of: self.test) { (text) in
                self.viewModel.updateText(text: text)
            }

The onChange modifier subscribes to changes in our @Binding property. So if it changes, these changes get passed to our SubViewModel. Note that your property needs to be Equatable. If you pass a more complex object, like a Struct, make sure to implement this protocol in your Struct.

onAppear

We need onAppear to handle the "first initial data" because onChange doesn't fire the first time your view gets initialized. It is only for changes.

.onAppear(perform: { self.viewModel.updateText(text: test) })

Ok and here is the SubViewModel, nothing more to explain to this one I guess.

class SubviewModel: ObservableObject {
    
    @Published var subViewText: String?
    
    func updateText(text: String?) {
        self.subViewText = text
    }
}

Now your data is in sync between your MainViewModel and SubViewModel and this approach works for large views with many subviews and subviews of these subviews and so on. It also keeps your views and corresponding viewModels enclosed with high reusability.

Working Example

Playground on GitHub: https://github.com/luca251117/PassingDataBetweenViewModels

Additional Notes

Why I use onAppear and onChange instead of only onReceive: It appears that replacing these two modifiers with onReceive leads to a continuous data stream firing the SubViewModel updateText multiple times. If you need to stream data for presentation, it could be fine but if you want to handle network calls for example, this can lead to problems. That's why I prefer the "two modifier approach".

Personal Note: Please don't modify the stateObject outside the corresponding view's scope. Even if it is somehow possible, it is not what its meant for.

Solution 5 - Swift

I guess I found a workaround for being able to control the instantiation of a view model wrapped with @StateObject. If you don't make the view model private on the view you can use the synthesized memberwise init, and there you'll be able to control the instantiation of it without problem. In case you need a public way to instantiate your view, you can create a factory method that receives your view model dependencies and uses the internal synthesized init.

import SwiftUI

class MyViewModel: ObservableObject {
    @Published var message: String

    init(message: String) {
        self.message = message
    }
}

struct MyView: View {
    @StateObject var viewModel: MyViewModel

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

public func myViewFactory(message: String) -> some View {
    MyView(viewModel: .init(message: message))
}

Solution 6 - Swift

@cicerocamargo's answer is a great suggestion. I had the same struggle in my app trying to figure out how to inject dependencies in my @StateObject view model and came up with the same answer after much testing. This way the view model will only be instantiated exactly once in all situations.

class MyViewModel: ObservableObject {
   @Published var dependency: Any

   init(injectedDependency: Any) {
       self.dependency = injectedDependency
   }
}

struct  MyView: View {
    @StateObject var viewModel: MyViewModel
    
    var body: some View {
       // ...
    } 
}

struct MyCallingView: View {
    var body: some View {
        NavigationLink("Go to MyView",
            destination: MyView(viewModel: MyViewModel(injectedDependency: dependencyValue)))
    }
}

The only thing to keep in mind with this, is that the view model's instantiation should be inline with the view's instantiation. If we change our calling view code to this:

struct MyCallingView: View {
    var body: some View {
        let viewModel = MyViewModel(injectedDependency: dependencyValue)
        NavigationLink("Go to MyView",
            destination: MyView(viewModel: viewModel))
    }
}

then the compiler won't be able to optimize this code and the viewModel will be instantiated every time MyCallingView gets invalidated and needs to redraw. On the upside, even if it is instantiated every time, only the original instance will be used.

Solution 7 - Swift

I don't really have a good solution for @StateObjects at the moment, but I was trying to use them in the @main App as the initialisation point for @EnvironmentObjects. My solution was not to use them. I am putting this answer here for people who are trying to do the same thing as me.

I struggled with this for quite a while before coming up with the following:

These two let declarations are at the file level

private let keychainManager = KeychainManager(service: "com.serious.Auth0Playground")
private let authenticatedUser = AuthenticatedUser(keychainManager: keychainManager)

@main
struct Auth0PlaygroundApp: App {

    var body: some Scene {
    
        WindowGroup {
            ContentView()
                .environmentObject(authenticatedUser)
        }
    }
}

This is the only way I have found to initialise an environmentObject with a parameter. I cannot create an authenticatedUser object without a keychainManager and I am not about to change the architecture of my whole App to make all my injected objects not take a parameter.

Solution 8 - Swift

I frequently visit this page whenever my views don't behave as they should. What I'm realising is I need to adapt my thinking from UIKit where I'd liberally use ViewModels to encapsulate a views state. I had much more confidence over the initialisation and teardown of these objects alongside my views. Using StateObject for a ViewModel with injected state is a bit of a black box and confusing. I think the answers on this post attest to this.

What I'm working towards now is a model proposed here https://nalexn.github.io/clean-architecture-swiftui/

I'll still use StateObject for strictly view properties but whenever I find myself wanting to inject state into the object I will consider it a possible code smell. For example a UserView having a UserViewModel(id: 1). I've tried both the injected viewModel and the init in the view with the _state = ... approaches and while they may work at first I've encountered bugs down the line.

The clean architecture linked above promotes a separate AppState which will pass its data onto views via bindings. Seems a bit Manager/Singleton ish but at least the management of my state is more obvious.

Solution 9 - Swift

Asperi's answer is great, but it seems not very perfect because of what document's said. I discovered following method, but I don't know if it is valid.

class Object: ObservableObject {
    let id: String
    init(id: String) {
        self.id = id
    }
}

struct ParentView: View {
    @State var obj: Object?
    var body: some View {
        if let obj = obj {
            ChildView().environmentObject(obj)
        } else {
            Button("Tap") {
                self.obj = Object(id: "id")
            }
        }
    }
}

struct ChildView: View {
    @EnvironmentObject var customObject: Object
    var body: some View {
        Text(customObject.id)
    }
}

Solution 10 - Swift

Really good answers.

Now, I found that in some cases, getting @StateObject right can be tricky, like handling network requests needed to retrieve information lazily, as the user navigates the UI.

Here's a pattern I like to use, especially when a screen (or hierarchy of screens) should present data lazily due to its associated retrieval cost.

It goes like this:

  • the main screen holds the model(s) for the child screen(s).
  • each model keeps track of its display state and whether it has already loaded the info. This helps avoid repeating costly ops, like network calls.
  • the child screen relies on the model and checks the display state to show a loading view or present the final information/error.

Here's the screen breakdown:

enter image description here

In a hurry? Here's the project:

https://github.com/tciuro/StateObjectDemo

Main screen (ContentView):

import SwiftUI

struct ContentView: View {
    @StateObject private var aboutModel = AboutModel()
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    NavigationLink(destination: AboutView(aboutModel: aboutModel)) {
                        Text("About...")
                    }
                } footer: {
                    Text("The 'About' info should be loaded once, no matter how many times it's visited.")
                }
                
                Section  {
                    Button {
                        aboutModel.displayMode = .loading
                    } label: {
                        Text("Reset Model")
                    }
                } footer: {
                    Text("Reset the model as if it had never been loaded before.")
                }
            }
            .listStyle(InsetGroupedListStyle())
        }
    }
}

Supporting datatypes:

enum ViewDisplayState {
    case loading
    case readyToLoad
    case error
}

enum MyError: Error, CustomStringConvertible {
    case loadingError
    
    var description: String {
        switch self {
            case .loadingError:
                return "about info failed to load... don't ask."
        }
    }
}

About Screen (AboutView):

import SwiftUI

struct AboutView: View {
    @ObservedObject var aboutModel: AboutModel
    
    var body: some View {
        Group {
            switch aboutModel.displayMode {
                case .loading:
                    VStack {
                        Text("Loading about info...")
                    }
                case .readyToLoad:
                    Text("About: \(aboutModel.info ?? "<about info should not be nil!>")")
                case .error:
                    Text("Error: \(aboutModel.error?.description ?? "<error hould not be nil!>")")
            }
        }
        .onAppear() {
            aboutModel.loadAboutInfo()
        }
    }
}

The AboutView model:

import SwiftUI

final class AboutModel: ObservableObject {
    private(set) var info: String?
    private(set) var error: MyError?
    
    @Published var displayMode: ViewDisplayState = .loading
    
    func loadAboutInfo() {
        /**
        If we have loaded the about info already, we're set.
        */
        
        if displayMode == .readyToLoad {
            return
        }
        
        /**
        Load the info (e.g. network call)
        */
        
        loadAbout() { result in
            /**
            Make sure we assign the 'displayMode' in the main queue
            (otherwise you'll see an Xcode warning about this.)
            */
            
            DispatchQueue.main.async {
                switch result {
                    case let .success(someAboutInfo):
                        self.info = someAboutInfo
                        self.displayMode = .readyToLoad
                    case let .failure(someError):
                        self.info = nil
                        self.error = someError
                        self.displayMode = .error
                }
            }
        }
    }
    
    /**
    Dummy function; for illustration purposes. It's just a placeholder function
    that demonstrates what the real app would do.
    */
    
    private func loadAbout(completion: @escaping (Result<String, MyError>) -> Void) {
        /**
        Gather the info somehow and return it.
        Wait a couple secs to make it feel a bit more 'real'...
        */
        
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            if Bool.random() {
                completion(.success("the info is ready"))
            } else {
                completion(.failure(MyError.loadingError))
            }
        }
    }
}

In short, I found that for this lazy loading pattern, placing the @StateObject in the main screen instead of the child screen avoids potentially unnecessary code re-executions.

In addition, using ViewDisplayState allows me to control whether a loading view should be shown or not, solving the common UI flickering issue that occurs when the data is already cached locally making the UI loading view not worth presenting.

Of course, this is not a silver bullet. But depending on your workflow it might be useful.

If you want to see this project in action and tinkle with it, feel free to download it here. Cheers! 酪

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionEmilio SchepisView Question on Stackoverflow
Solution 1 - SwiftAsperiView Answer on Stackoverflow
Solution 2 - SwiftMarkView Answer on Stackoverflow
Solution 3 - SwiftAndrew BogaevskyiView Answer on Stackoverflow
Solution 4 - SwiftLucaView Answer on Stackoverflow
Solution 5 - SwiftcicerocamargoView Answer on Stackoverflow
Solution 6 - SwiftmikeView Answer on Stackoverflow
Solution 7 - SwiftBrettView Answer on Stackoverflow
Solution 8 - SwiftMorgzView Answer on Stackoverflow
Solution 9 - SwiftDan LeeView Answer on Stackoverflow
Solution 10 - SwifttitusmagnusView Answer on Stackoverflow