【譯】在 iOS 中使用 MVVM

簡介

建立 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

  1. 點擊 New Group
  2. 重命名文件夾爲 ViewModel
  3. 重複以上操做,建立名爲 Controller 的文件夾
  4. 將 ViewController.swift 拖入 Controller 文件夾

建立 ViewModel

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:設計模式

  1. 在 ViewModel 文件夾上呼出菜單
  2. 點擊 New File
  3. 選擇 swift File,點擊 Next
  4. 命名爲 WindViewModel

在新建的文件中添加如下代碼: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)"
    }
    
}
複製代碼
  1. 建立突變函數(mutating function),讓咱們可以更改 Struct 內的屬性。
  2. 爲每一個屬性建立不一樣的設置函數。

目前這些方法都很是簡單,但隨着學習的深刻,它們可能會變得更加複雜,尤爲是當咱們在 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

至此,咱們已經建立並實現了咱們須要的 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 界面,用來展現咱們指望的天氣信息:

  • 在 main.storyboard 的 ViewController 中拖入四個 UILabel,作好約束。
  • 實現如下四個 IBOutlet,與 storyboard 中的四個 label 進行鏈接:
@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 的翻譯,但並不是嚴格意義上的逐字逐句翻譯,但願你能讀得通順,順便讀得愉快。

相關文章
相關標籤/搜索