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