ViewModels interoperability with MVVM Architecture | SwiftUI

Hi,

I’ve been encountering use cases where I can’t help but want to make my ViewModels communicate with each other. Imagine an app with several ViewModels as EnvironmentObjects, where each handles rather extensive business logics about different things.

As an example and for the sake of discussion, imagine there’s first a WeatherViewModel that handles business logic about weather data, and second a StoreViewModel that handles business logic notably about the frequency of visits to a store.

Say there was a view in which we would like to plot the probability of rain against frequency of visits, compute correlations and whatnot… (Again… for the sake of discussion)

We would need to make calculations with data that comes from 2 separate viewmodels.

What are some best practices for this type of problem?

For now I was thinking of creating a third viewmodel that has access to the other two. It could communicate with them either via a sort of dependency injection:

class ThirdViewModel: ObservableObject {
    let fvm: FirstViewModel
    let svm: SecondViewModel
    init(fvm: FirstViewModel, svm: SecondViewModel) {
        self.fvm = fvm
        self.svm = svm
    }
    
    @Published var somePropertiesToDisplayInTheView:[Stuff] = []
    
    func doComputationsWithFvmAndSvmData() async {
        
    }
    
}

Or I was thinking of simply injecting the view models not necessarily in the whole thirdviewmodel, but only in a specific method like this:

 func doComputationsWithFvmAndSvmData(fvm:FirstViewModel,svm:SecondViewModel) async {
        
    }

And then have the view call this function.

I don’t want to centralize everything at the view level either, I try to avoid having too much logic there. At the same time, the solutions I mentioned feel a bit weird and I also don’t want to run into data races issues.

Has anyone encountered questions like this and how did you choose to solve it?

Many thanks!

Raph

So I didn’t get too many replies haha but just in case somebody ends up here, here’s an update…

For lack of a better idea I implemented the solutions that I thought felt weird and it works pretty well!
If you’re curious, the foundation works like this:

struct EnvironmentObjectsDeclarationView: View {
    
    let fvm:FirstViewModel
    let svm:SecondViewModel
    let tvm:ThirdViewModel
    
    init() {
        // Some networking class that conforms to some network protocols
        let dataService = DataService()
        
        // ViewModels
        self.fvm = FirstViewModel(network:dataService)
        self.svm = SecondViewModel(network:dataService)
        self.tvm = ThirdViewModel(fvm:fvm, svm:svm)
    }
    
    var body: some View {
        ContentView()
            .environmentObject(fvm)
            .environmentObject(svm)
            .environmentObject(tvm)
    }
}

That way the third view model has access to everything in the first and second view models, including data fetching methods that may involve networking, and it’s all abstracted away.

What I’m not a big fan of though is that in the tvm I also have access to a bunch of methods from fvm and svm that I don’t need. Like I may only need a couple methods for each, but not the whole thing.

If you're reading this and you know a better way - please hit me up!

Thanks
Raph

Huh.

I lowkey just rubberducked myself while writing this but I guess I could make the first and second view models conform to some protocols that declare what the third view model needs.

I actually still wouldn’t do it this way

You’re using multiple instances of the dataService and I think this will create bugs because you’re not injecting it in a way where it’ll still react to state changes

Your goal should be to only inject specifically what properties something needs, because of less issues with side effects, but if that’s not possible you can inject a whole object (but I think the way you’re currently doing it will lead to issues)

I can try to look back at this later or you can email me mikaela@icyappstudio.com and I can do 1 hour of paid mentoring, cause this is actually a big question that requires more background info

Thanks for your reply Mikaela, I appreciate it!

I’m not sure I follow. There’s only one instance of DataService, of which the first and second viewmodels have a reference.

Totally agree with that goal. That’s partly why I don’t like my solution and why I’m looking for something better.

Regarding side effects and other issues (were you referring to crashes from data races?):
As far as I understand, they might occur if some of the requests involve writing data - especially data that depend on a state. But I’m mostly doing all this to read data from different view models and centralize it so that should be fine? Just in case, I made my dataservice object an actor instead of a class hoping for the best.

Thanks for the offer Mikaela. Right now money is too tight so I have to respectfully decline and just keep coding. But I do plan on finding mentors when we launch the business as I definitely need guidance to advance from enthusiastic junior dev to the next level, so I’ll keep that it mind in the future!

Ahhh I see, lol I somehow missed this when I read it the first time

One way to help with this is making variables private(set) so they can only be changed in the class they’re created in.

I totally understand! My advice overall, is it’s better to spend the money / time to do something correctly, and it will pay benefits in the end, because you can then apply concepts for the rest of the prototype and get any and all questions you have answered. (Because you can send me questions and the codebase ahead of time, where I can look at it and have context around what you’re doing).
If you change your mind, just email me!


Some issues with this implementation, you have an EnvironmentObjectsDeclarationView which I’m guessing passes these view models down every view. Meaning every view in your app has access to the whole app state, and that means every view can change it, which is not usually a good idea.

Second this is a View and your view models don’t have a property wrapper like ObservableObject on them. So because of how SwiftUI works, they’re getting constantly created and destroyed which can definitely lead to issues.
If those concepts don’t make sense, that’s also why I’d recommend at least booking a single hour with me

Haha no prob, I understand you’re reading fast

Cool! I’m already doing that mostly to prevent myself from writing ugly code by changing properties from Views, but I didn’t know it helped with side effects! :smiley: Thanks

Totally agree, I’m obsessed with writing code as clean as I can! I’ll give you a heads up :slight_smile:

Yeah I’ve read a few articles that argue against EnvironmentObjects. What I remember is you basically end up with a bunch of singletons and it’s weird to have access to so much logic globally in the app.

On the other hand, our app basically requires that I have access to everything everywhere all at once, and I’m the only dev responsible for data flow in the codebase, so I just gotta trust myself not to mess things up. Also, I don’t fully understand how to work with @StateObject and @ObservedObject so I’m sticking to what I know for our first app haha

I’m guessing you’re referring to the @ObservedObject property wrapper?

I fail to understand why the view models would be constantly created and destroyed yet their data still persists, and I also don’t understand why the @ObservedObject property wrapper would prevent them from being destroyed.

This stuff seems pretty essential though so I’ll keep doing research and meditate on the matter haha.

Thanks a lot Mikaela, I’ll email you when I’m ready

It’s all about how state is managed in SwiftUI. You may be doing something that’s not shown in your example so they’re not. But they’re constantly created and destroyed because SwiftUI views are constantly created and destroyed (that’s how they work, and refresh)

And putting something in an initializer means that it’s being created and destroyed, unless you use the correct property wrappers that makes it hold it’s state through being created / destroyed.

It goes back to how structs are value types and classes are reference types