What is the proper way to instantiate an observable ViewModel with init parameters in a SwiftUI View?

During the development of our new SwiftUI apps we found issues while trying to implement well-known patterns such as MVVM. Searching online is tough because blogs can state different answers for the same problem, and Apple hasn't been very clear about this in the documentation.

Requirements:

  • Ensure single initialization
    • If not properly used, the framework might lead you to accidentally make the View reinstantiate the ViewModel during its execution
  • Initialization parameters
    • We sometimes need to provide the ViewModel with parameters for initialization in the following data flow pattern: parentView.body -> childView.init(params) -> childViewModel.init(params)
  • Testing
    • We also want to be able to provide a completely different ViewModel to the view in case we're testing it, this should be possible if the mocked VM follows the same protocol of the normal VM

Our current solution doesn't meet all requirements yet:

struct ParentView: View {
  var body: some View {
    ChildView(someValue: "foo")
  }
}

class ChildViewModel: ObservableObject {
  init(someValue: Any) {
  }
}

struct ChildView: View {
  @StateObject var vm: ChildViewModel
  init(someValue: Any) {
    self._vm = StateObject(wrappedValue: .init(someValue: someValue)
  }
}

Is there an example project that implements some or even all of the above requirements, while meeting Apple's guidelines and best practices? Does our current solution have any immediately noticeable mistakes?

Thanks in advance.

Post not yet marked as solved Up vote post of feijo Down vote post of feijo
2.6k views

Replies

This question comes up so frequently that it would be extremely appreciated if someone on the SwiftUI team or a DTS representative could chime in on this. The vast majority of sample code and documentation assumes a view model can be created without any parameters to its initializer, which is not always the case. And in the Fruta example app a single Model is used for the entire application, which really isn't realistic for larger scale applications.

I've resorted to the following design pattern but I remain unsure if this is considered a "correct" way to initialize an @StateObject property:

struct ParentView: View {
  var body: some View {
    ChildView(viewModel: ChildViewModel(someValue: "foo"))
  }
}

class ChildViewModel: ObservableObject {
  init(someValue: Any) {
  }
}

struct ChildView: View {
  @StateObject var viewModel: ChildViewModel
}

This pattern appears to work correctly and doesn't require the small "hack" of using the underscore to initialize the @StateObject, which appears to be discouraged based on my reading of the documentation:

StateObject.init(wrappedValue:)

// 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:

struct MyView: View {
    @StateObject var model = DataModel()
}
  • On your example, the line where you'd pass the parameter to the view model is missing. Can you clarify? Because if you try to instantiate the view model and assign it to the StateObject during the init, it fails because the variable when annotated with StateObject is get-only.

Add a Comment