I'm building an app in SwiftUI, based on the MVVM design pattern. What I'm doing is this:
struct AddInvestment: View {
@ObservedObject private var model = AddInvestmentVM()
@State private var action: AssetAction?
@State private var description: String = ""
@State private var amount: String = ""
@State private var costPerShare: String = ""
@State private var searchText: String = ""
@State var asset: Asset?
var body: some View {
NavigationView {
VStack {
Form {
Section("Asset") {
NavigationLink(model.chosenAsset?.completeName ?? "Scegli asset") {
AssetsSearchView()
}
}
Section {
Picker(action?.name ?? "", selection: $action) {
ForEach(model.assetsActions) { action in
Text(action.name).tag(action as? AssetAction)
}
}
.pickerStyle(.segmented)
.listRowBackground(Color.clear)
}
Section {
TextField("", text: $amount, prompt: Text("Unità"))
.keyboardType(UIKit.UIKeyboardType.decimalPad)
TextField("", text: $costPerShare, prompt: Text("Prezzo per unità"))
.keyboardType(UIKit.UIKeyboardType.decimalPad)
}
}
}
.navigationTitle("Aggiungi Investimento")
}
.environmentObject(model)
.onAppear {
model.fetchBaseData()
}
}
}
Then I have my ViewModel, this:
class AddInvestmentVM: ObservableObject {
private let airtableApiKey = "..."
private let airtableBaseID = "..."
private let assetsTableName = "..."
private let assetsActionsTableName = "..."
private let airtable = Airtable.init("...")
private var tasks = [AnyCancellable]()
@Published var chosenAsset: Asset?
@Published var assets = [Asset]()
@Published var assetsActions = [AssetAction]()
init() { }
func fetchBaseData() {
print("Fetching data...")
let assetActionsRequest = AirtableRequest.init(baseID: airtableBaseID, tableName: assetsActionsTableName, view: nil)
let assetsActionsPublisher: AnyPublisher<[AssetAction], AirtableError> = airtable.fetchRecords(request: assetActionsRequest)
assetsActionsPublisher
.eraseToAnyPublisher()
.sink { completion in
print("** **")
print(completion)
} receiveValue: { assetsActions in
print("** **")
print(assetsActions)
self.assetsActions = assetsActions
}
.store(in: &tasks)
}
}
Now, as you can see I have some properties on the view that are binded to some text fields. Let's take these in consideration:
@State private var description: String = ""
@State private var amount: String = ""
@State private var costPerShare: String = ""
@State private var searchText: String = ""
Keeping in mind the MVVM pattern, should these properties be declared in the ViewModel and binded from there? Or is this a right approach?
CodePudding user response:
Instead of giving you a concrete answer which concrete variable you might move to the view model, I would like to give you a more general answer, which might also help you in the decision of other use cases, and eventually should help to answer your question yourself ;)
A View Model publishes the binding (I am not talking about a @Binding here
!), which completely and unambiguously describes what a view shall render. The how it actually looks like may be still part of the view.
Tip: define a struct which contains all the variables constituting the binding, then publish this struct in your view model. You may name this binding
ViewState
.
If we take this strict, it means in other words, that for each possible value of the binding, there is one and only one visual representation of the view.
However, in practice, it is favourable to relax this a bit, otherwise you would have to specify and handle even the scrolling position of a scroll view, too. How far you go with this may depend on the complexity of your view and the whole scenario. But the rule, that the binding is the definitive source of truth for the view and determines what it is rendering should not be violated.
Generally, in a scenario where your model is not mutable (i.e. there are no user actions which alter the model), you very rarely would use @State
variables in a SwiftUI view, since there is nothing which is variable.
Even when it happens that the elements in the list change or the order or the number of elements change, the list view always receives a constant array of elements whose elements are also not mutable by the user. Thus, you use a let elements: [Element]
in your SwiftUI view.
On the other hand, your view model may use a model which publishes a value of [Element]
and the view model observes it, and then mutates the binding accordingly.
Another principle of MVVM is (actually any MV* pattern) that your view does not perform logic on its own and it never ever changes the binding. Instead, your view signals the user's "intent" via call backs (aka actions, events, intents) which eventually will reach the view model and then handled there, which in turn may eventually affect the binding.
Strictly, that would mean, if you have a scenario where a user can edit a string, and IFF you use a binding for this string, the string cannot be mutated by editing of the user. Instead, each editing command will be send to the view model, the view model then handles the changes, and sends back the mutated binding which eventually will reflect the user's changes.
Also, in practice there might be more favourable solutions: a view might use its own "edit buffer" for the string, using a @State
variable. This string gets mutated "internally" within the view when the user edits the text. The model only gets informed when the user "commits" the changes, or when the user taps the "back" or "done" button. This approach is favourable when the "editing" itself is simple and no complex validation is required during editing, or when there is no need to trigger side effects (like updating a suggestion list in a search bar). Otherwise, you would route the editing through the view model.
Again in practice, you decide how far you go with a view that has "it's own behaviour". It depends on the use case and what is favourable in terms of avoiding unnecessary complex solutions which don't make the solution any better through being too strict to the pattern.
Conclusion:
In MVVM, you use @State
variables in Views only iff your View adds a behaviour which does not strictly need to be monitored by the View Model and does not violate the rule that the Binding is the definitive source of truth from the perspective of the view and that the View Model still is the authority for the performed logic.
CodePudding user response:
I tend to think of the View as being the definition of the human interface of an app for a device or set of devices (iPhone, iPad, Mac, etc). I tend to think of the Model as the place where the applications logic goes that is not part of any View (business logic, data manipulation, network manipulation, etc). I think of the ViewModel as being the place where any logic exits that the View needs to make the View work, such as formatting data for display on the View. I think you are looking for where the single source of truth lives for a data element needed by the View. To my thinking it should only live in the ViewModel if and only if it is specific to the View. Any logic or data that is about the application should live in the Model, not the ViewModel. By doing this I make my Model more reusable, more independent of the View. There are always borderline cases and I think about if this data or logic would need to be recreated if I built an entirely separate second View, say one that worked with a Browser via HTML/JavaScript/Etc. If I would have to recreate that data or logic with a new View layer, then I think the data or logic belongs in the Model. If the data or logic is specific to this View layer, then I would put it in the ViewModel for that View.
Using the above thinking your AddInvestment class looks like something I would put in the Model, not the ViewModel. Of course, it is all a matter of taste and I do not think there is any one right answer here, but that is how I think I would slice and dice it in my own apps.