如何从头开始在Swift中实现MVVM模式

在深入研究代码之前,这里是对该模式的快速介绍。 MVVM代表模型,视图,视图模型,这是一种特定的体系结构,其中视图模型位于视图和模型之间,提供模仿UI组件的接口。 通过“绑定”值(将逻辑数据链接到UI)来建立此连接。

在iOS应用中,这种MVVM方法中的View元素通常是UIViewController本身。 这种方法的好处是代码变得更加模块化,易于维护和测试。

现在让我们进入代码。

模型

与api模型匹配,这是我的两个结构开始。

struct CurrencyRate { 
let currencyIso : String let rate : Double
}
 struct Converter { 
let base : String
let date : String
let rates : [CurrencyRate]
}

我还创建了一个特定的服务来创建一个api请求以及我的Converter实现的解析器协议。 您可以在该文章末尾的Github存储库中找到这些源代码。 这并不是真正与MVVM相关的,因此我将直接进入我们的ViewModel和绑定系统。

视图模型

首先,为了能够将ViewModel的值绑定到View,我们需要具有可观察模式的元素。 在iOS中,我们可以使用KVO模式添加和删除观察者,但我认为我们可以使用“ didSet”观察者做得更好。

让我们记住,我们要随着时间的推移执行特定的代码,而该代码已更改。 我们还可以假设我们的对象可以有多个等待更新的观察者。

为此,我创建了一个类,其中包含一个观察者及其更新时要执行的代码的字典(此处为闭包)。 在这里看起来像什么。

 typealias CompletionHandler = (() -> Void) 
 class DynamicValue { 
var value : T { didSet { self.notify() } }
private var observers = [String: CompletionHandler]()
  init(_ value: T) { 
self.value = value
}
  public func addObserver(_ observer: NSObject, completionHandler: @escaping CompletionHandler) { 
  observers[observer.description] = completionHandler 
}
  public func addAndNotify(observer: NSObject, completionHandler: @escaping CompletionHandler) { 
  self.addObserver(observer, completionHandler: completionHandler) 
self.notify()
}
  private func notify() { 
observers.forEach({ $0.value() })
}
  deinit { 
observers.removeAll()
}
}

对于我的示例,我想将ViewModel分离到数据层。 因此,我创建了一个通用数据源,无论我们要更新的数据如何,我们都可以重用它。

 class GenericDataSource : NSObject { 
var data: DynamicValue = DynamicValue([])
}

现在我们可以实现ViewModel,能够获取货币汇率并在获取数据源后对其数据源进行更新。 为了使其更安全,更容易测试(通过依赖项注入),我的CurrencyViewModel将不拥有dataSource。

  struct CurrencyViewModel { 
弱var dataSource:GenericDataSource ?
  init(dataSource:GenericDataSource ?){ 
self.dataSource =数据源
}
  func fetchCurrencies(){ 
CurrencyService.shared.fetchConverter {结果为
DispatchQueue.main.async {
切换结果{
case .success(let converter)://重新加载数据
self.dataSource?.data.value = converter.rates
打破
案例。失败(让错误):
print(“解析器错误\(错误)”)
打破
}
}
}
}
}

另一方面,让我们创建将绑定到我们的视图的数据源。

  class CurrencyDataSource:GenericDataSource ,UITableViewDataSource { 
func numberOfSections(在tableView中:UITableView)-> Int {
返回1
}
  func tableView(_ tableView:UITableView,numberOfRowsInSection部分:Int)-> Int { 
返回data.value.count
}
  func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath)-> UITableViewCell { 
让cell = tableView.dequeueReusableCell(withIdentifier:“ CurrencyCell”,for:indexPath)为! 货币单元

让currencyRate = self.data.value [indexPath.row]
cell.currencyRate = currencyRate
 返回单元 
}
}

提醒一下,我将CurrencyDataSource与CurrencyDataSource分开的主要原因是您的ViewModel永远不应该知道绑定到的View。 通过实现UITableViewDataSource ,我觉得它离它太近了,所以我将其分隔为另一个类:CurrencyDataSource

如果明天您想更改UICollectionView的UI,则不需要更新ViewModel,我们将有很多关注点。

视图

最后,让我们实现我们的View,即我们的CurrencyViewController。 在那里需要做的是将UITableView链接到其dataSource,而且还绑定值以在有新数据可用时自动刷新UI。

  class CurrencyViewController:UIViewController { 
@IBOutlet弱var tableView:UITableView!
 让dataSource = CurrencyDataSource() 
懒惰的var viewModel:CurrencyViewModel = {
让viewModel = CurrencyViewModel(dataSource:dataSource)
返回viewModel
}()
 覆盖func viewDidLoad(){ 
super.viewDidLoad()
self.title =“英镑汇率”
self.tableView.dataSource = self.dataSource
  self.dataSource.data.addAndNotify(观察者:自我){[弱自我]在 
self?.tableView.reloadData()
}
  self.viewModel.fetchCurrencies() 
}
  ... 

这里的另一个技巧是,通过使用addAndNotify同时添加和更新观察者,我们的tableView将被重新加载,因此即使没有数据,我们的UI也将在首次启动时构建。

总而言之,我们终于完成了MVVM方法:将每一层分开,使其更易于使用,维护和测试 。 它也更具可读性,我们的文件平均由50行代码组成。

我在这篇文章中省略了一些我认为与模式本身并不完全相关的内容,例如我用于处理api请求,解析和处理异常的服务。 Github上提供了整个示例iOS应用程序。

最后,我相信测试和具有挑战性的模式是使自己对此有想法的最好方法。 这是让您对代码更有信心的好方法。 另一方面,我不会只依赖一个解决方案,而忽略其他选择,这也是我创建该模板的原因:一旦想到一个新项目,我就可以重用和重新思考它。