1

I'm working on a SwiftUI app using the MVVM architecture. I have the problem that I need to pass data from a parent View into its child viewModel, but I'm not sure how to pass a parameter from the view into the viewModel.

The child View which should own the viewModel is here:

struct InstanceView: View {
    @ObservedObject var instance: Instance
    
    @StateObject var viewModel = InstanceViewViewModel(instance: instance)

    var body: some View {
        ...
    }
}

And this is the viewModel:

class InstanceViewViewModel: ObservableObject {
    @ObservedObject var instance: Instance
    
    ...
}

Obviously the View doesn't work, I get the error Cannot use instance member 'instance' within property initializer; property initializers run before 'self' is available

If I try using init() in the View to assign a value to viewModel:

@ObservedObject var instance: Instance

@StateObject var viewModel: InstanceViewViewModel

init(instance: Instance) {
    self.instance = instance
    self.viewModel = InstanceViewViewModel(instance: instance)
}

But I get the error Cannot assign to property: 'viewModel' is a get-only property.

I have read that you should give the viewModel property (instance) a default value, and then set it in the View with .onAppear(). However, in my case, Instance is a Core Data object so I can't really create one to use as a default value.

I also read that I could maybe use _instance = StateObject(wrappedValue: InstanceViewViewModel(instance: instance)) in the View's init, and swiftUI would be smart enough to only initialize the StateObject once. However I'm pretty sure this is a bad practice.

I also can't use lazy on a StateObject

So is there any other way to achieve this? Could a solution be somehow getting the value InstanceView is being initialized with on this line:

@StateObject var viewModel = InstanceViewViewModel(instance: instance)

, outside of an init, so I don't need to reference self, like you would do inside of an init? (this is probably a stupid idea)

Or should I be implementing MVVM in a different way?

Thanks!

Cameron Delong
  • 454
  • 1
  • 6
  • 12
  • Yes, you should be implementing MVVM differently. The viewModel is your source of truth and it should be independent of the view. What is the parameter that you are trying to initialize it with? – Yrb Jul 23 '21 at 02:03
  • 1
    "However I'm pretty sure this is a bad practice." -- why? Like you alluded to, `StateObject` uses an auto closure in its creation, so it'll only be created once (confirmed in a WWDC lab I had this summer). Utilizing either `init` or `onAppear` was what the SwiftUI engineers recommended to me. – jnpdx Jul 23 '21 at 02:18
  • 1
    Storing an `@ObservedObject` inside an `ObservableObject` doesn't make sense. Maybe you mean `@Published`? – jnpdx Jul 23 '21 at 02:19
  • This might be helpful https://stackoverflow.com/a/68480486/14733292 – Raja Kishan Jul 23 '21 at 03:39
  • Does this answer your question https://stackoverflow.com/a/62636048/12299030? – Asperi Jul 23 '21 at 06:02
  • Does this answer your question? [Initialize @StateObject with a parameter in SwiftUI](https://stackoverflow.com/questions/62635914/initialize-stateobject-with-a-parameter-in-swiftui) – Andrew Jul 23 '21 at 06:56
  • @Yrb can you help me understand how I should be implementing mvvm? Most of the guides I've read have had the view owning the viewModel in a stateObject. I want to initiate it with an `Instance`, which is a Coredata object. In the parent view a NavigationView passes the selected `Instance` into the `InstanceView`. I need the `InstanceViewViewModel` to know which instance is selected, so that's why I want to pass it in. – Cameron Delong Jul 23 '21 at 17:03
  • @jnpdx in a thread I was reading about this problem (suggested by asperi and Andrew), multiple people said that it was a bad practice. From the apple documentation: "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." – Cameron Delong Jul 23 '21 at 17:10
  • I hadn't thought about how using `@ObservedObject` inside of an `ObservableObject` would cause a problem, but I'm doing because `Instance` is an `NSManagedObjectContext`, so it conforms to `ObservableObject` and I want to observe it. I had a similar issue in the same VM, where I have a stopwatch `ObservableObject` class, and needed to observe it inside the VM. My solution was to use a custom publisher: https://pastebin.com/Z0VtYSZe (I'm not sure if this is a good was to do that but it works), and I'll probably add the same thing for `instance`. Is there another approach I should take? – Cameron Delong Jul 23 '21 at 17:15
  • @Asperi @Andrew I did come across that thread before, but I didn't use any of the solutions in it for reasons I mentioned in my post. The accepted answer seemed to be a bad practice, and the second answer won't work for me since I can't really set a default value to `instance`, since it's a Core Data object – Cameron Delong Jul 23 '21 at 17:19
  • @CameronDelong Since Apple won't let you record lab sessions, I can't cite a source, but the SwiftUI engineers did not have a problem with using `init` for `StateObject` at all. You definitely don't want to do *any* heavy lifting there, but since the initializer for `StateObject` uses an auto closure and is only run once, there's no danger. – jnpdx Jul 23 '21 at 19:49
  • @CameronDelong in terms of using `@ObservedObject` inside an `ObservableObject` I understand what you're trying to accomplish, but the property wrapper isn't doing any good, since it isn't vending its update status to its parent like it does within a `View`. – jnpdx Jul 23 '21 at 19:50
  • @jnpdx Alright I'll go with that solution, thanks for the info! How would you suggest I observe an `ObservableObject` in my ViewModel? Using `@ObservedObject` with a custom publisher seemed to work for my `Stopwatch` class in the same situation as I described before. At the moment I don't think I actually need to observe the changes to `instance`, since it shouldn't change while `InstanceView` is showing, but that's probably going to change so I will need a way to accomplish that. – Cameron Delong Jul 23 '21 at 21:18
  • I'd use Combine in one way or another to subscribe to the changes -- basically what you're doing in the `Stopwatch` example. That being said, I'm not a CoreData user myself, so maybe there's a more clever way. – jnpdx Jul 23 '21 at 22:26
  • Alright that’s probably what I’ll do. Unfortunately I don’t think there is currently any intuitive way to do it. Thanks for all the help! – Cameron Delong Jul 23 '21 at 22:57

3 Answers3

1

SwiftUI property wrappers are often automagic. @StateObject modifies the property in a way that the initialization has to be at the point of declaration. The pattern suggested by Apple is to create the @StateObject in the parent of the view and pass it down either as an @ObservedObject or @EnvironmentObject.

Морт
  • 1,163
  • 9
  • 18
0

@StateObject can be confusing. Use it if, and only if, that view owns that data forever.

In your example, that doesn't seem to be the case because InstanceView declares a viewModel as an instance of an observable object.

View models aren't usually created inside a view but rather passed or injected to it.

From your description, I think your viewModel should be declared as an @Binding in InstanceView and then passed to the child view again as a binding.

This way, the viewModel owns the data and the views can read and change them but not own them.

Also, your view model seems wrong, you don't declare the properties as @ObservedObject, rather you publish them, so something like this:

class MyViewModel: ObservableObject {
  @Published var name: String // or whatever
}

struct MyView: View {
@Binding var viewModel: MyViewModel

Text(viewModel.name)

Hope this helps.

kakubei
  • 5,321
  • 4
  • 44
  • 66
  • Why would one use `@Binding` on an `ObservableObject`? – jnpdx Jul 20 '23 at 15:40
  • Why not? @Binding is used when your view doesn't own the data, but needs to read it or change it. – kakubei Jul 21 '23 at 16:06
  • Are you talking about in the iOS 17 beta? Otherwise, `@Binding` is used for _value_ types. Not `ObservableObject`s, which are reference types. You would just pass it via `@ObservedObject` in that case – jnpdx Jul 21 '23 at 16:08
-2

It's not good practice to use MVVM view model objects with SwiftUI because in SwiftUI, the View struct value type is designed to hold the view data which is regarded as less buggy than using objects. The use of property wrappers, e.g. @State give these value types similar semantics to reference types, i.e. objects, so we get the best of both worlds. If you move your view data into view model objects instead of using the View struct correctly, then you lose these features and re-introduce the consistency bugs that SwiftUI was designed to eliminate. These structs form an efficiently updated (via dependency tracking) and diffed hierarchy and SwiftUI internally takes the result and they update UIKit (or AppKit) controls for us in a platform specific way, we don’t interact with the actual UIViews, NSView i.e. the V in the traditional MVC/MVVM sense at all.

The ‘ObservableObject’ protocol for reference types is part of the Combine framework so we only use that when we require a Combine pipeline which doesn’t appear to be required in your case. ‘@StateObject’ defines a source of truth, thus it cannot possibly take any parameters because then it wouldn’t be a source of truth.

malhal
  • 26,330
  • 7
  • 115
  • 133