【译】在 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 的翻译,但并不是严格意义上的逐字逐句翻译,但愿你能读得通顺,顺便读得愉快。

相关文章
相关标签/搜索