建立 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 的翻譯,但並不是嚴格意義上的逐字逐句翻譯,但願你能讀得通順,順便讀得愉快。