为每个UITableViewCell创建视图模型

tes*_*sla 26 design-patterns mvvm uitableview ios swift

我坚持一个设计决策,为表视图的单元格创建视图模型.每个单元的数据由数据源类(具有数组Contacts)提供.在MVVM唯一的视图模型可以谈的模式,但它没有意义把数据源视图模型,因为这将使得有可能为所有单元存取数据,也这是错误的把数据源视图控制器,因为它必须没有参考数据.还有一些其他关键时刻:

  • 每个单元格必须拥有自己的视图模型实例,而不是共享实例
  • cellForRowAtindexPath 不得放在视图模型中,因为它不应包含任何UI引用
  • View/ViewController的视图模型不应与单元的视图模型交互

什么是为插入MVVM关系中的单元格"插入"数据源的正确方法?谢谢.

Jor*_*tiz 86

让我从一些理论开始.MVVM是Microsoft Silverlight和WPF 的Presentation Model(或Application Model)的专业化.这种UI架构模式背后的主要思想是:

  • 视图部分是唯一依赖于GUI框架的部分.这意味着对于iOS,视图控制器是视图的一部分.
  • 视图只能与视图模型对话.永远不要去模特.
  • 视图模型保存视图的状态.通过视图模型属性向视图提供此状态.这些属性不仅包含标签的值,还包含其他视图相关信息,如启用了保存按钮或评级视图的颜色.但是国家的信息必须是独立于UI框架的.因此,对于iOS,颜色的属性应该是枚举,例如,而不是UIColor.
  • 视图模型还提供了处理UI操作的方法.此操作将与模型通信,但它们永远不会更改与数据直接相关的视图状态.相反,它会与模型进行对话并询问所需的更改.
  • 模型应该是自治的,即您应该能够为命令行应用程序和UI界面使用相同的模型代码.它将处理所有业务逻辑.
  • 该模型不了解视图模型.因此,视图模型的更改通过观察机制传播.对于iOS和具有普通NSObject子类甚至核心数据的模型,可以使用KVO(也适用于Swift).
  • 一旦视图模型知道模型中的更改,它应该更新它所拥有的状态(如果使用值类型,那么它应该创建一个更新的并替换它).
  • 视图模型不了解视图.在其最初的概念中,它使用数据绑定,这不适用于iOS.因此,视图模型中的更改通过观察机制传播.你也可以在这里使用KVO,或者正如你在问题中提到的那样,一个简单的委托模式,如果与Swift属性观察者结合使用会更好.有些人更喜欢反应式框架,比如RxSwift,ReactiveCocoa,甚至是Swift Bond.

正如您所提到的那样好处:

  • 更好地分离关注点.
  • UI独立性:更轻松地迁移到其他UI.
  • 由于关注点的分离和代码的解耦性质,因此具有更好的可测试性.

回到你的问题,UITableViewDataSource协议的实现属于体系结构的视图部分,因为它依赖于UI框架.请注意,为了在代码中使用该协议,该文件必须导入UIKit.另外像方法tableView(:cellForRowAt:)返回一个观点是严重依赖的UIKit.

然后,您的数组Contacts(确实是您的模型)无法通过视图(数据源或其他方式)进行操作或查询.相反,您将视图模型传递给表视图控制器,在最简单的情况下,它具有两个属性(我建议它们存储,而不是计算属性).其中一个是节数,另一个是每节的行数:

var numberOfSections: Int = 0
var rowsPerSection: [Int] = []
Run Code Online (Sandbox Code Playgroud)

视图模型使用对模型的引用进行初始化,并且作为初始化的最后一步,它设置这两个属性的值.

视图控制器中的数据源使用视图模型的数据:

override func numberOfSections(in tableView: UITableView) -> Int {
    return viewModel.numberOfSections
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return viewModel.rowsPerSection[section]
}
Run Code Online (Sandbox Code Playgroud)

最后,您可以为每个单元格设置不同的视图模型结构:

struct ContactCellViewModel {
    let name: String

    init(contact: Contact) {
        name = contact.name ?? ""
    }
}
Run Code Online (Sandbox Code Playgroud)

UITableViewCell子类就知道如何使用的结构:

class ContactTableViewCell: UITableViewCell {
    
    var viewModel: ContactCellViewModel!

    func configure() {
        textLabel!.text = viewModel.name
    }
}
Run Code Online (Sandbox Code Playgroud)

为了获得每个单元格的相应视图模型,表视图视图模型将提供生成它们的方法,并且可以用于填充视图模型的数组:

func viewModelForCell(at index: Int) -> ContactCellViewModel {
    return ContactCellViewModel(contact: contacts[index])
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这里的视图模型是唯一与模型(您的Contacts数组)交谈的模型,并且视图仅与视图模型对话.

希望这可以帮助.

  • 虚拟机应该保留视图的状态,因此每次生成新虚拟机并不是一个好主意。相反,为 VC 的 VM 中的单元创建 VM 列表,并将 VM 传递给 VC 以获取不同的索引路径。 (2认同)

Lit*_*hor -6

除非您有一个可以解决的特定问题,否则Model-View-ViewModel尝试仅将其用于“最佳实践”将最终引入许多不必要的复杂性。

您的数据源负责填充您的表。除了您的数据源之外,没有任何东西需要引用,contacts因为它将使用此数据更新您的表。

View Models仅当您需要进行复杂的 UI 交互和更新时才发挥作用。VM 负责封装视图的状态,例如......

  1. 文本字段的值
  2. 选择了哪些复选框/单选按钮
  3. 元素的颜色
  4. 动画逻辑
  5. UI 元素之间的依赖关系

当您的视图发生更改时,您View Model有责任对您的视图进行更新(必要时),以反映通过 UIModel对其进行的更改。Model

话虽如此,视图模型在 IOS 中没有意义,因为 IOSView Controllers在设计方法中使用了称为MVC(模型-视图-控制器)的设计方法

  • 首先,我使用 MVVM 并不是因为它很奇特,而是因为它使编写测试变得更容易,它分离了 MVC 所没有的表示逻辑和业务逻辑(因为视图控制器设计)。其次,MVVM在iOS上很好,它只是MVC的升级版本。第三,因为 ViewController 和 View 耦合得如此紧密,所以这种等式几乎 100% 正确:ViewController = View,Apple 版本的 MVC 看起来更像是这样:View/ViewController -> Model。因此 View/ViewController 可以与 Model 对话,而 Model 仅在必须执行更新时通知 View/ViewController。 (4认同)