Swift中的MVVM

MVVM是MVC体系结构的增强版本,我们在其中正式连接视图和控制器,并将业务逻辑移出控制器并移入视图模型。 MVVM听起来可能很复杂,但它实际上是您已经熟悉的MVC体系结构的修饰版本。 一般来说,MVVM通常与功能性反应式编程结合使用,并且有很多FRP库,例如RxSwift和ReactiveCocoa。 但是,如果不熟悉FRP,仍然可以在项目中利用MVVM。 在本文中,我将演示如何在不使用FRP库的情况下采用MVVM。

模型视图控制器

让我们开始使用标准的MVC方法,该示例将在UITableView显示一个排序整数数组。 此外,我们可以通过单击右上角的添加按钮以正确的顺序插入新的整数,并通过滑动行来删除整数。

在这里,我只是创建一个名为DemoViewControllerUITableViewController子类,并在其中实现必要的UITableViewDataSource方法。 另外,我还将插入逻辑放在addNewInteger方法中,将删除逻辑放在tableView(_ tableView:, commit editingStyle:, forRowAt indexPath:)方法中。

 final class DemoViewController: UITableViewController { 
fileprivate var sortedIntegers = [1, 2, 3]
  override func viewDidLoad() { 
super.viewDidLoad()
  tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.description()) 
  let addButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewInteger)) 
navigationItem.rightBarButtonItem = addButtonItem
}
  func addNewInteger() { 
let number = Int(arc4random_uniform(10))
let insertionIndex = sortedIntegers.upperBoundary(of: number)
sortedIntegers.insert(number, at: insertionIndex)
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: insertionIndex, section: 0)], with: .automatic)
tableView.endUpdates()
}
}
 extension DemoViewController { 
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sortedIntegers.count
}
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 
let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.description(), for: indexPath)
cell.textLabel?.text = "\(sortedIntegers[indexPath.row])"
return cell
}
  override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 
return true
}
  override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { 
guard editingStyle == .delete else { return }
  sortedIntegers.remove(at: indexPath.row) 
tableView.beginUpdates()
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
}

模型视图视图模型

转换为MVVM的第一步是创建一个称为Statestruct ,它存储与表视图相关的信息,在我们的示例中是排序后的整数。

 struct State { 
private(set) var sortedIntegers: [Int]
  func text(at indexPath: IndexPath) -> String { 
return "\(sortedIntegers[indexPath.row])"
}
}

因为在插入或删除整数之后应该更新表视图,所以我们编写一个名为EditingStyleenum ,它包含insertdeletenone情况。 此外,我们在State创建了editingStyle属性,并使用editingStyle属性上的Swift属性观察器功能更新了sortedIntegers属性。

 struct State { 
enum EditingStyle {
case insert(Int, IndexPath)
case delete(IndexPath)
case none
}
  var editingStyle: EditingStyle { 
didSet {
switch editingStyle {
case let .insert(new, indexPath):
sortedIntegers.insert(new, at: indexPath.row)
case let .delete(indexPath):
sortedIntegers.remove(at: indexPath.row)
default:
break
}
}
}
  // ... 
}

现在,我们可以轻松创建视图模型类,它包括插入和删除逻辑。 此外,我们的视图控制器只能通过我们的视图模型访问State信息。 如果State的信息发生了变化,我们的视图控制器也可以通过视图模型的回调关闭来更新其表视图。

 final class DemoViewModel { 
private(set) var state = State(sortedIntegers: [1, 2, 3]) {
didSet {
callback(state)
}
}
let callback: (State) -> ()
  init(callback: @escaping (State) -> ()) { 
self.callback = callback
}
  func addNewInteger() { 
let integer = Int(arc4random_uniform(10))
let insertionIndex = state.sortedIntegers.upperBoundary(of: integer)
state.editingStyle = .insert(integer, IndexPath(row: insertionIndex, section: 0))
}
  func removeInteger(at indexPath: IndexPath) { 
state.editingStyle = .delete(indexPath)
}
}

最后,我们修改视图控制器中的代码以连接所有内容。 我们创建一个viewModel属性,并首先在viewDidLoad方法的末尾实例化它。

 final class DemoViewController: UITableViewController { 
fileprivate var viewModel: DemoViewModel?
  override func viewDidLoad() { 
super.viewDidLoad()
  // ... 
  viewModel = DemoViewModel { [unowned self] (state) in 
switch state.editingStyle {
case .none:
self.tableView.reloadData()
case let .insert(_, indexPath):
self.tableView.beginUpdates()
self.tableView.insertRows(at: [indexPath], with: .automatic)
self.tableView.endUpdates()
case let .delete(indexPath):
self.tableView.beginUpdates()
self.tableView.deleteRows(at: [indexPath], with: .automatic)
self.tableView.endUpdates()
}
}
}
}

然后,我们用DemoViewModel方法替换addNewIntegerUITableViewDataSource方法中的先前代码,一切都准备就绪。

 final class DemoViewController: UITableViewController { 
// ...
  func addNewInteger() { 
viewModel?.addNewInteger()
}
}
 extension DemoViewController { 
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel?.state.sortedIntegers.count ?? 0
}
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 
let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.description(), for: indexPath)
cell.textLabel?.text = viewModel?.state.text(at: indexPath)
return cell
}
  override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 
return true
}
  override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { 
guard editingStyle == .delete else { return }
  viewModel?.removeInteger(at: indexPath) 
}
}

结论

示例游乐场在这里,本文的灵感来自objc.io Swift Talk。

在您的项目中采用MVVM有很多优点。 首先,它使代码库更具可测试性。 视图控制器始终在测试方面享有盛誉,但是将所有业务逻辑移入视图模型将为编写逻辑测试提供方便和可能性。 其次,遵循这种模式可以使代码库更加一致,并带来更高的可读性。 此外,可以通过FRP库进一步降低UI绑定和异步链接的复杂性。 我完全愿意讨论和反馈,所以请分享您的想法。 谢谢!