Emilio Schepis
Emilio Schepis

Reputation: 1997

Initialize @StateObject with a parameter in SwiftUI

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.

Upvotes: 179

Views: 59957

Answers (12)

Fiser
Fiser

Reputation: 43

My solution is the following.

I put the id to trigger the refresh of the state in the parent that injects the ViewModel to the child view like that.

Why the id? because if I don't put it it does not trigger the refresh of the content of the view model.

if let vm = globalState.selected {
    ChildView(state: ChildViewModel(vm: vm))
       .id(vm.id)
}

struct ChildView: View {
  @StateObject var vm: ChildViewModel
  ...
}

I don't know the implications of that but it's working in my case because I require the behaviour that I descrived I hope to have helped

Upvotes: -1

Asperi
Asperi

Reputation: 257493

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

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

From Apple (for all those like @user832):

confirmation

Upvotes: 181

Bruno Coelho
Bruno Coelho

Reputation: 946

Based on Andrew Bogaevskyi answer, I created an extension to StateObject with a init that emulates a closure and avoids creating a new instance of StateObject every time.

extension StateObject {
    @inlinable init(_ value: @escaping () -> ObjectType) where ObjectType : ObservableObject {
        self.init(wrappedValue: value())
    }
}

Here is the testing code

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 = StateObject {
            ContentViewModel()
        }
        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()
            
            // 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() }
        }
    }
}

extension StateObject {
    @inlinable init(_ value: @escaping () -> ObjectType) where ObjectType : ObservableObject {
        self.init(wrappedValue: value())
    }
}

And the output

ContentView init
ViewModel init
ContentView init
ContentView init
ContentView init
ContentView init
ContentView init

Upvotes: 2

titusmagnus
titusmagnus

Reputation: 2082

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! 🤙🏻

Upvotes: 1

Andrew Bogaevskyi
Andrew Bogaevskyi

Reputation: 2615

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.

Upvotes: 62

Dan Lee
Dan Lee

Reputation: 111

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

Upvotes: 0

Morgz
Morgz

Reputation: 574

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.

Upvotes: 0

mike
mike

Reputation: 31

@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.

Upvotes: 3

Brett
Brett

Reputation: 1837

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.

Upvotes: 1

Luca
Luca

Reputation: 221

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.

Upvotes: 4

cicerocamargo
cicerocamargo

Reputation: 1072

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

Upvotes: 6

Mark
Mark

Reputation: 18184

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

Upvotes: 57

Related Questions