视图中带有变量的SwiftUI MVVM方法



我正在SwiftUI中构建一个基于MVVM设计模式的应用程序。我正在做的是:

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()
}
}
}

然后我有我的ViewModel,这个:

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)
}
}

现在,正如您所看到的,我在视图中有一些属性绑定到一些文本字段。让我们考虑一下:

@State private var description: String = ""
@State private var amount: String = ""
@State private var costPerShare: String = ""
@State private var searchText: String = ""

请记住MVVM模式,这些属性是否应该在ViewModel中声明并从那里绑定?或者这是一个正确的方法?

我不想给你一个具体的答案,你可以将哪个具体的变量移到视图模型中,而是想给你提供一个更一般的答案,这可能也有助于你决定其他用例,最终应该有助于自己回答你的问题;)

视图模型发布绑定(我不是在谈论@Binding here!),完全明确描述了视图应呈现的内容。的实际外观可能仍然是视图的一部分。

提示:定义一个包含构成绑定的所有变量的结构,然后在视图模型中发布此结构。您可以将此绑定命名为ViewState

如果我们采用这种严格的方法,换句话说,这意味着对于绑定的每个可能值,都有一个并且只有一个视图的视觉表示。

然而,在实践中,稍微放松一下是有利的,否则您甚至还必须指定和处理滚动视图的滚动位置。你能走多远可能取决于你的观点和整个场景的复杂性。但不应违反绑定是视图真实性的最终来源并确定其呈现内容的规则。

通常,在模型不可变的情况下(即没有改变模型的用户操作),您很少会在SwiftUI视图中使用@State变量,因为没有变量

即使列表中的元素发生变化或元素的顺序或数量发生变化,列表视图也始终接收一个常量元素数组,该数组的元素也不可由用户更改。因此,您可以在SwiftUI视图中使用let elements: [Element]

另一方面,您的视图模型可能使用一个发布[Element]值的模型,视图模型会对其进行观察,然后相应地更改绑定

MVVM的另一个原则是(实际上是任何MV*模式),您的视图不单独执行逻辑它永远不会更改绑定。相反,您的视图表示用户的";意图";通过回调(也称为操作、事件、意图),最终将到达视图模型,然后在那里进行处理,这反过来可能最终影响绑定。

严格地说,这意味着,如果你有一个场景,用户可以编辑一个字符串,并且IFF你为这个字符串使用绑定,则不能通过编辑用户来更改字符串。相反,每个编辑命令都将被发送到视图模型,然后视图模型处理更改,并发回最终反映用户更改的变异绑定。

此外,在实践中可能有更有利的解决方案:一种观点可能会使用自己的"观点";编辑缓冲区";对于字符串,使用@State变量。这个字符串变了";内部";当用户编辑文本时,在视图中。只有当用户";提交";改变或当用户点击"按钮"时;背面";或";完成";按钮这种方法是有利的;编辑";它本身很简单,在编辑过程中,或者在不需要触发副作用的情况下(比如更新搜索栏中的建议列表),不需要复杂的验证。否则,您将在视图模型中进行编辑。

再次在实践中,你决定你的观点走多远;这是自己的行为;。这取决于用例,以及在避免不必要的复杂解决方案方面什么是有利的,这些解决方案不会因为对模式过于严格而使解决方案变得更好。

结论:

在MVVM中,只有当您的视图添加了一种行为时,才可以在视图中使用@State变量,该行为不需要由视图模型严格监控,并且不违反绑定是从视图角度来看真理的最终来源的规则,并且视图模型仍然是执行逻辑的权威。

我倾向于认为View是一个设备或一组设备(iPhone、iPad、Mac等)的应用程序的人机界面的定义。我倾向于将模型视为应用程序逻辑的所在地,而不是任何视图(业务逻辑、数据操作、网络操作等)的一部分。我认为ViewModel是视图工作所需的任何逻辑退出的地方,例如格式化要在视图上显示的数据。我认为您正在为View所需的数据元素寻找真理的单一来源。在我看来,它应该只存在于ViewModel中,当且仅当它特定于视图。任何与应用程序有关的逻辑或数据都应该存在于Model中,而不是ViewModel中。通过这样做,我使我的模型更加可重用,更加独立于视图。总是有边界情况,我想如果我构建了一个完全独立的第二个视图,比如通过HTML/JavaScript/Etc与浏览器一起工作的视图,是否需要重新创建这些数据或逻辑。如果我必须用一个新的View层重新创建那个数据或逻辑,那么我认为数据或逻辑属于Model。如果数据或逻辑特定于此视图层,那么我会将其放在该视图的ViewModel中。

使用上面的想法,AddInvestment类看起来像是我放在Model中的东西,而不是ViewModel中的。当然,这都是品味的问题,我不认为这里有任何一个正确的答案,但我认为我会在自己的应用程序中对其进行分割。

最新更新