通过MVVM使您的视图控制器节食
在本系列的上一篇文章中,我写了关于Model-View-Controller模式及其一些缺陷的文章。 尽管MVC给软件开发带来了明显的好处,但在大型或复杂的Cocoa应用程序中,它往往不尽人意。
不过,这不是新闻。 多年来,出现了几种架构模式,旨在解决“模型-视图-控制器”模式的缺点。 例如,您可能听说过MVP ,Model-View-Presenter和MVVM ,Model-View-ViewModel。 这些模式看起来和感觉都类似于Model-View-Controller模式,但是它们也解决了Model-View-Controller模式所遇到的一些问题。
1.为什么选择Model-View-ViewModel
在我偶然发现Model-View-ViewModel模式之前,我已经使用Model-View-Controller模式多年了。 MVVM成为可可社区的后来者也就不足为奇了,因为它的起源可以追溯到微软。 但是,MVVM模式已移植到Cocoa并适应Cocoa框架的要求和需求,并且最近在Cocoa社区中越来越受到关注。
最具吸引力的是MVVM感觉像是Model-View-Controller模式的改进版本。 这意味着它不需要剧烈改变思维定势。 实际上,一旦您了解了该模式的基础知识,就可以轻松实现它,而不比实现Model-View-Controller模式更困难。
2.节食View Controller
在上一篇文章中 ,我写道典型的Cocoa应用程序中的控制器与原始MVC模式中定义的Reenskaug控制器有些不同。 例如,在iOS上,视图控制器控制视图。 它的唯一职责是填充其管理的视图并响应用户交互。 但这不是大多数iOS应用程序中视图控制器的唯一责任,不是吗?
MVVM模式向组合中引入了第四个组件,即视图模型 ,它有助于重新定位视图控制器。 它通过接管视图控制器的某些职责来做到这一点。 请看下面的图,以更好地了解视图模型如何适合Model-View-ViewModel模式。

如图所示,视图控制器不再拥有模型。 拥有模型的是视图模型,并且视图控制器向视图模型询问其需要显示的数据。
这是与“模型-视图-控制器”模式的重要区别。 视图控制器无法直接访问模型。 视图模型将其需要显示在视图中的数据交给视图控制器。
视图控制器及其视图之间的关系保持不变。 这很重要,因为这意味着视图控制器可以专注于填充其视图和处理用户交互。 这就是视图控制器的设计目的。
结果非常引人注目。 视图控制器节食,许多责任转移到视图模型。 您不再需要一个跨越数百甚至数千行代码的视图控制器。
3.视图模型的责任
您可能想知道视图模型如何适应更大的画面。 视图模型的任务是什么? 它与视图控制器有何关系? 那模型呢?
我之前显示给您的图表为我们提供了一些提示。 让我们从模型开始。 该模型不再由视图控制器拥有。 视图模型拥有该模型,并且充当视图控制器的代理。 每当视图控制器需要其视图模型中的一条数据时,后者就会向其模型询问原始数据,并以使其可以立即在其视图中使用的方式对其进行格式化。 视图控制器不负责数据操作和格式化。
该图还显示该模型归视图模型所有,而不是视图控制器。 还值得指出的是,Model-View-ViewModel模式尊重视图控制器及其视图的紧密关系,这是Cocoa应用程序的特征。 这就是为什么MVVM感觉很自然地适合可可应用的原因。
4.一个例子
由于Model-View-ViewModel模式不是Cocoa固有的,因此没有严格的规则来实现该模式。 不幸的是,许多开发人员对此感到困惑。 为了澄清一些事情,我想向您展示一个使用MVVM模式的应用程序的基本示例。 我们创建了一个非常简单的应用程序,该应用程序从Dark Sky API获取预定义位置的天气数据,并将当前温度显示给用户。
步骤1:建立专案
启动Xcode并基于Single View Application模板创建一个新项目。 我在本教程中使用Xcode 8和Swift 3。

将项目命名为MVVM ,并将Language设置为Swift并将Devices设置为iPhone 。

步骤2:建立检视模型
在由模型-视图-控制器模式提供支持的典型Cocoa应用程序中,视图控制器将负责执行网络请求。 您可以使用管理器来执行网络请求,但是视图控制器仍会知道天气数据的来源。 更重要的是,它将接收原始数据,并且需要在将其显示给用户之前对其进行格式化。 这不是我们采用Model-View-ViewModel模式时采用的方法。
让我们创建一个视图模型。 创建一个新雨燕的文件,将其命名WeatherViewViewModel.swift,并定义了一个名为类WeatherViewViewModel
。

import Foundation
class WeatherViewViewModel {
}
这个想法很简单。 视图控制器向视图模型询问预定义位置的当前温度。 由于视图模型将网络请求发送到Dark Sky API,因此该方法接受闭包,当视图模型具有用于视图控制器的数据时,将调用该闭包。 该数据可能是当前温度,但也可能是错误消息。 这就是视图模型的currentTemperature(completion:)
方法的外观。 稍后我们将详细填写。
import Foundation
class WeatherViewViewModel {
// MARK: - Type Alias
typealias CurrentTemperatureCompletion = (String) -> Void
// MARK: - Public API
func currentTemperature(completion: @escaping CurrentTemperatureCompletion) {
}
}
为了方便起见,我们声明了类型别名,并定义了一个方法currentTemperature(completion:)
,该方法接受类型为CurrentTemperatureCompletion
的闭包。
如果您熟悉网络和URLSession
API,则实现起来并不难。 看一下下面的代码,请注意我使用了枚举API
来使所有内容保持整洁。
import Foundation
class WeatherViewViewModel {
// MARK: - Type Alias
typealias CurrentTemperatureCompletion = (String) -> Void
// MARK: - API
enum API {
static let lat = 37.8267
static let long = -122.4233
static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
static let baseURL = URL(string: "https://api.darksky.net/forecast")!
static var requestURL: URL {
return API.baseURL
.appendingPathComponent(API.APIKey)
.appendingPathComponent("\(lat),\(long)")
}
}
// MARK: - Public API
func currentTemperature(completion: @escaping CurrentTemperatureCompletion) {
let dataTask = URLSession.shared.dataTask(with: API.requestURL) { [weak self] (data, response, error) in
// Helpers
var formattedTemperature: String?
if let data = data {
formattedTemperature = self?.temperature(from: data)
}
DispatchQueue.main.async {
completion(formattedTemperature ?? "Unable to Fetch Weather Data")
}
}
// Resume Data Task
dataTask.resume()
}
}
我尚未向您展示的唯一代码是temperature(from:)
方法的实现。 在这种方法中,我们从Dark Sky响应中提取当前温度。
// MARK: - Helper Methods
func temperature(from data: Data) -> String? {
guard let JSON = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else {
return nil
}
guard let currently = JSON?["currently"] as? [String : Any] else {
return nil
}
guard let temperature = currently["temperature"] as? Double else {
return nil
}
return String(format: "%.0f °F", temperature)
}
在生产应用程序中,我将选择一个更强大的解决方案来解析响应,例如ObjectMapper或Unbox 。
步骤3:整合检视模型
现在,我们可以在视图控制器中使用视图模型。 我们为视图模型创建一个属性,并且还为用户界面定义了三个出口。
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
@IBOutlet var temperatureLabel: UILabel!
// MARK: -
@IBOutlet var fetchWeatherDataButton: UIButton!
// MARK: -
@IBOutlet var activityIndicatorView: UIActivityIndicatorView!
// MARK: -
private let viewModel = WeatherViewViewModel()
}
注意,视图控制器拥有视图模型。 在此示例中,视图控制器还负责实例化其视图模型。 通常,我更喜欢将视图模型注入到视图控制器中,但是现在让我们保持简单。
在视图控制器的viewDidLoad()
方法中,我们调用一个辅助方法fetchWeatherData()
。
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Fetch Weather Data
fetchWeatherData()
}
在fetchWeatherData()
,我们向视图模型询问当前温度。 在请求温度之前,我们将隐藏标签和按钮并显示活动指示器视图。 在闭包中,我们传递给fetchWeatherData(completion:)
,我们通过填充温度标签并隐藏活动指示器视图来更新用户界面。
// MARK: - Helper Methods
private func fetchWeatherData() {
// Hide User Interface
temperatureLabel.isHidden = true
fetchWeatherDataButton.isHidden = true
// Show Activity Indicator View
activityIndicatorView.startAnimating()
// Fetch Weather Data
viewModel.currentTemperature { [unowned self] (temperature) in
// Update Temperature Label
self.temperatureLabel.text = temperature
self.temperatureLabel.isHidden = false
// Show Fetch Weather Data Button
self.fetchWeatherDataButton.isHidden = false
// Hide Activity Indicator View
self.activityIndicatorView.stopAnimating()
}
}
该按钮连接到动作fetchWeatherData(_:)
,其中我们还调用了fetchWeatherData()
帮助器方法。 如您所见,helper方法可帮助我们避免代码重复。
// MARK: - Actions
@IBAction func fetchWeatherData(_ sender: Any) {
// Fetch Weather Data
fetchWeatherData()
}
步骤4:创建用户界面
难题的最后一步是创建示例应用程序的用户界面。 打开Main.storyboard ,然后在垂直堆栈视图中添加标签和按钮。 我们还将在堆栈视图的顶部(垂直和水平居中)添加活动指示器视图。

不要忘记连接电源和我们在ViewController
类中定义的动作!
现在构建并运行该应用程序以进行尝试。 请记住,您需要Dark Sky API密钥才能使应用程序正常工作。 您可以在Dark Sky网站上注册免费帐户。
5.有什么好处?
即使我们仅将一点点的片段移至视图模型,您可能仍想知道为什么这样做是必要的。 我们获得了什么? 为什么要增加这一额外的复杂性层?
最明显的收获是视图控制器更精简,更专注于管理其视图。 这是视图控制器的核心任务:管理其视图。
但是有一个更微妙的好处。 由于视图控制器不负责从Dark Sky API获取天气数据,因此它不知道与该任务有关的详细信息。 天气数据可能来自其他天气服务或缓存的响应。 视图控制器不会知道,也不需要知道。
测试也大大改善。 众所周知,由于视图控制器与视图层的紧密关系,因此很难对其进行测试。 通过将一些业务逻辑移至视图模型,我们可以立即提高项目的可测试性。 测试视图模型非常容易,因为它们没有到应用程序视图层的链接。
结论
Model-View-ViewModel模式是设计Cocoa应用程序的重要一步。 视图控制器不是那么庞大,视图模型更易于编写和测试,因此您的项目变得更易于管理。
在这个简短的系列文章中,我们只是从头开始。 关于Model-View-ViewModel模式,还有很多要写的东西。 多年来,它已成为我最喜欢的模式之一,这就是为什么我不断对此进行讨论和写作的原因。 试试看,让我知道您的想法!
翻译自: https://code.tutsplus.com/tutorials/put-your-view-controllers-on-a-diet-with-mvvm--cms-29473