The approach I take to defining ViewModels for SwiftUI was heavily inspired by Paul Hudson’s post Introducing MVVM into your SwiftUI project. In it, he advocates for defining class ViewModel inside an extension to the relevant view. By naming each view’s MVVM class ViewModel, it can always be referenced directly by that name instead of remembering to use ContentViewViewModel, UserListViewModel, etc. I take it a step further and simply name the class Model:
In practice, useful property wrappers like @Binding or @Environment will get hoisted into the view. I’ve also had absolutely zero luck incorporating this fully with SwiftData. In theory you can build your own Query with Predicate<PersistentModel> and SortDescriptor<PersistentModel>, but in practice I’m always getting bitten by oblique EXC_BAD_ACCESS and EXC_BREAKPOINT failure states caused by various race conditions, so @Query in the view is my current approach. I’ll still let the model do things like filter results for searchable(text:) though, by calling the model’s filter(_:) function with the View’s @Query var passing the result to a ForEach(_:, content:) or similar.
With this approach, my default is that any function or variable that the view needs should be put into the model. When a subview needs data from the parent, it’ll either be passed in as the value owned by the model: PersonView(person: model.person) or in cases where a mixture of data and behavior are required, I’ll pass in the entire model: SettingsView(model: model). Each of these subviews are expected to have their own Model class, and so I end up with init methods that take whatever arguments the view’s model needs to have passed to them and builds their own models: