建立 App 时有许多不一样的架构能够选择,其中使用最为普遍的是 MVC (Model-View-Controller),虽然如今 MVC 因为缺乏一些结构层面的抽象,常常被戏称为 Messive View Controller (Messive - 笨重的)。这篇文章咱们就来研究一下如何在 iOS 应用中使用 MVVM (Model-View-ViewModel) 这种设计模式。ios
MVVM 的使用让咱们可以从 ViewController 中提取出一些页面显示逻辑,让咱们可以为每一个 View 分别自定义不一样的 Model,即 ViewModel (固然,若是有须要,咱们仍然能复用这些 ViewModel )。好比,咱们从 API 获取一个字符串 "Hello World",但在页面上但愿它展现为 "Hello World, how are you?",若是有这种需求,在每一个 View 中对字符串进行修改没太大意义,这时候就须要请 ViewModel 入场了。git
正确的工程文件目录结构对于使用 MVVM 来讲很重要,正确的结构可以使咱们的项目更易于维护。在基础文件夹 WeatherForecast 上呼出菜单(鼠标-单击右键,触控板-双指单击):github
ViewController 获取到一个 CurrentWeather 对象,结构以下:json
struct CurrentWeather: Codable {
let coord: Coord
let weather: [WeatherDetails]
let base: String
let main: Main
let visibility: Int
let wind: Wind
let clouds: Clouds
let dt: Int
let sys: Sys
let id: Int
let name: String
}
复制代码
除了 String 和 Int 这种基础类型的对象外,它还包括其余类型的对象,咱们须要使用 ViewModel 解析出咱们真正须要的信息。好比,这个 ViewController 可能只对风力(wind)、坐标(coord)和 名称(name) 信息感兴趣。swift
咱们如今开始建立一个 ViewModel:设计模式
在新建的文件中添加如下代码:api
// ViewModel/WindViewModel.swift
import Foundation
struct WindViewModel {
let currentWeather: CurrentWeather
init(currentWeather: CurrentWeather) {
self.currentWeather = currentWeather
}
}
复制代码
以上,咱们新建了一个 ViewModel 而且自定义了 initializer,以便咱们能够从 ViewController 传入 CurrentWeather 对象。这个 ViewModel 将对 CurrentWeather 对象中的数据进行操做,以便咱们能够按照需求展现咱们须要的信息。这种方式能够将一些逻辑方法从 ViewController 中剥离出来,为 ViewController 瘦身。网络
下面,咱们要根据界面需求,为 ViewModel 增长一些属性。这里有一个原则,就是只增长界面中必要的属性:架构
private(set) var coordString = ""
private(set) var windSpeedString = ""
private(set) var windDegString = ""
private(set) var locationString = ""
复制代码
注意 private(set)
,它表示这个属性可以在此文件外进行读取,但只能在此文件中进行修改。下面咱们将加入一些设置这些属性的方法,完成后这个文件会是这样的:async
// ViewModel/WindViewModel.swift
import Foundation
struct WindViewModel {
let currentWeather: CurrentWeather
private(set) var coordString = ""
private(set) var windSpeedString = ""
private(set) var windDegString = ""
private(set) var locationString = ""
init(currentWeather: CurrentWeather) {
self.currentWeather = currentWeather
}
private mutating func updateProperties() {
coordString = setCoordString(currentWeather: currentWeather)
windSpeedString = setWindSpeedString(currentWeather: currentWeather)
windDegString = setWindDirectionString(currentWeather: currentWeather)
locationString = setLocationString(currentWeather: currentWeather)
}
}
extension WindViewModel {
private func setCoordString(currentWeather: CurrentWeather) -> String {
return "Lat: \(currentWeather.coord.latitude), Lon: \(currentWeather.coord.longtitude)"
}
private func setWindSpeedString(currentWeather: CurrentWeather) -> String {
return "Wind Speed: \(currentWeather.wind.speed)"
}
private func setWindDirectionString(currentWeather: CurrentWeather) -> String {
return "Wind Deg: \(currentWeather.wind.deg)"
}
private func setLocationString(currentWeather: CurrentWeather) -> String {
return "Location: \(currentWeather.name)"
}
}
复制代码
目前这些方法都很是简单,但随着学习的深刻,它们可能会变得更加复杂,尤为是当咱们在 CurrentWeather 对象中使用可选值(optional value)以后。假如咱们使用 MVC 模式,并且必须在 ViewController 内的许多地方访问这些可选值,咱们会看到 ViewController 的代码量大幅提高,并且会充满了 guard 声明。然而,MVVM 模式使得咱们可以对每一个可选值只使用一次 guard 声明,好比,若是 location 变量是可选的,咱们只须要这么作:
private func setLocationString(currentWeather: CurrentWeather) -> String {
guard let name = currentWeather.name else {
return "Location not available"
}
return "Location: \(name)"
}
复制代码
这样咱们就能够在 ViewController 中随意访问 locationString
变量,没必要在对它是否为 nil 进行检验,由于咱们在 ViewModel 中已经进行了检验。
至此,咱们已经建立并实现了咱们须要的 ViewModel,接下来就能够在 ViewController.swift 中使用它了。
在 ViewController.swift 类 private let apiManager = APIManager()
之下写入如下代码:
private(set) var windViewModel: WindViewModel?
var searchResult: CurrentWeather? {
didSet {
guard let searchResult = searchResult else { return }
windViewModel = WindViewModel.init(currentWeather: searchResult)
}
}
复制代码
这样就实例化了一个只容许在 ViewController 中进行修改的 WindViewModel 对象,还实例化了一个带有 didset 属性观察器的 CurrentWeather 对象,名为 searchResult。didset 属性观察器意味着当其对应的对象被设置或改变后,观察器中的方法就会运行。在咱们的示例中,这个观察器会实例化一个临时的 CurrentWeather 对象,用来存储上面的 self.searchResult 实例,它也叫 searchResult,而后初始化一个 WindViewModel 实例,在初始化方法中将临时的 searchResult 实例传入。如今,咱们须要作的就是在请求 API 成功的回调方法中对 self.searchResult 进行赋值。
在 ViewController 的 extension 中实现 getWeather() 方法:
private func getWeather() {
apiManager.getWeather() { (weather, error) in
if let error: Error = error {
print("Get weather error: \(error.localizedDescription)")
return
}
guard let weather = weather else { return }
self.searchResult = weather
}
}
复制代码
咱们对 self.searchResult 进行了赋值,下面将实现简单的 UI 界面,用来展现咱们指望的天气信息:
@IBOutlet weak var locationLabel: UILabel!
@IBOutlet weak var windSpeedLabel: UILabel!
@IBOutlet weak var windDirectionLabel: UILabel!
@IBOutlet weak var coordLabel: UILabel!
复制代码
而后,在 ViewController 的 extension 中实现 updateLabels() 方法:
private func updateLabels() {
guard let windViewModel = windViewModel else { return }
locationLabel.text = windViewModel.locationString
windSpeedLabel.text = windViewModel.windSpeedString + " m/s"
windDirectionLabel.text = windViewModel.windDegString
coordLabel.text = windViewModel.coordString
}
复制代码
注意,咱们的 windViewModel 实例是可选类型,所以咱们须要保证在访问其属性时它已经被赋值,而不是 nil。这也是为何咱们在 self.searchResult 中对临时 searchResult 进行赋值时使用 guard 语句的缘由。初始化一个非 nil 的 windModel 后,在主线程中对 UI 进行更新,self.searchResult 的 didset 属性观察器会变成这样:
var searchResult: CurrentWeather? {
didSet {
guard let searchResult = searchResult else { return }
windViewModel = WindViewModel.init(currentWeather: searchResult)
DispatchQueue.main.async {
self.updateLabels()
}
}
}
复制代码
这时候,咱们就能够运行这个 App 了,它会自动请求网络,若是成功,四条天气信息将会展现在四个 label 上。
示例工程地址:thisisluke/WeatherForecast
MVVM 能够避免咱们写出过于臃肿的 ViewController,咱们可以将大部分业务逻辑从 MVC 风格的 ViewController 中抽离出来,从而使代码复用性更高。固然,对于十分简单的工程来讲,MVVM 看起来有些复杂,但随着工程体积和业务逻辑的不断提升,MVVM 的优点会变的很明显。至因而否使用 MVVM,或是否在某个模块使用 MVVM,还要看具体需求后再决定。
这篇文章是对 Using MVVM in iOS 的翻译,但并不是严格意义上的逐字逐句翻译,但愿你能读得通顺,顺便读得愉快。