美文网首页
通过重构把MVC转变为MVVM

通过重构把MVC转变为MVVM

作者: 行知路 | 来源:发表于2021-07-10 10:26 被阅读0次

一、引言

        本文把一个MVC的工程通过重构来使其转变为MVVM的架构,并且在其中学习到MVVM的子组件以及其优势。

二、MVVM简介

        模型-视图-视图模型 (MVVM) 是近年来在 iOS 开发社区中受到关注的一种设计模式。它涉及一个称为视图模型的新概念。 在 iOS 应用中,视图模型(ViewModel)与视图控制器紧密相关。


MVVM架构

        像上图展示的那样,MVVM模式包含3个层次:

  • Model:App所需要的数据;与MVC相比,此层没有发生变化
  • View:用户界面视觉元素, 在iOS中,视图控制器与视图的概念是分不开的;与MVC相比,此层包含MVC层中的ViewController
  • ViewModel:负责处理Model与View层之间的交互;与MVC相比,此层大部分内容是从MVC模式中的Controller层剥离出来的内容;ViewModel的职责包括
    • Model 的输入:处理VIew的输入并且更新Model
    • Model的输出:把Model的输出传递给ViewController
    • 格式化:把Model的数据以供ViewController显示

        与MVC模式相比,MVVM具有以下优势:

  • 降低复杂性:MVVM通过把大量业务逻辑从View Controller迁出而是它变得简单
  • 富有表现力:ViewModel能够更好的表达了View的业务逻辑
  • 增强可测性:视图模型比视图控制器更容易测试,无需担心视图实现即可测试业务逻辑

        在MVVM模式中,ViewControler具有较大的变化:

  • 在MVC模式中ViewController居于核心位置,其负责驱动整个模式的运转,承担的是控制器的工作
  • 在MVVM中ViewController的重要性大幅减低,其转变为View的Contoller(控制器)——只用于控制VIew、把View的输入传递给View Model

三、从MVC重构为MVVM

3.1 熟悉原工程

        请下载示例工程并打开Begin文件夹中的工程。此App从weatherbit.io获取最新的信息并展现一些基础天气信息。为了从weatherbit.io获取天气信息,需要在 https://www.weatherbit.io/account/create进行注册。注册后会获得一个Key,并进行替换。

替换Key
App运行界面
App的文件结构
        在Controllers文件夹的WeatherViewController.swift文件是我们本次重构的重点。Utilities、View Models文件夹目前是空的,我们通过重构来把此文件夹填满。
// WeatherViewController.swift文件的私有属性

// geocoder 接受一个字符串输入,例如华盛顿特区,并将其转换为纬度和经度,然后发送给气象服务。
private let geocoder = LocationGeocoder() 

// 默认的区域
private let defaultAddress = "McGaheysville, VA"

// 格式化日期用于显示
private let dateFormatter: DateFormatter = {
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = "EEEE, MMM d"
  return dateFormatter
}()

// 格式化温度
private let tempFormatter: NumberFormatter = {
  let tempFormatter = NumberFormatter()
  tempFormatter.numberStyle = .none
  return tempFormatter
}()


override func viewDidLoad() {
  // 通过默认地址来获取对应的经纬度
  geocoder.geocode(addressString: defaultAddress) { [weak self] locations in
    guard let self = self, let location = locations.first  else { 
        return 
    }

    // 获得经纬度所制定的名字
    self.cityLabel.text = location.name

    // 使用经纬度来获取数据
    self.fetchWeatherForLocation(location)
  }
}

func fetchWeatherForLocation(_ location: Location) {
  // 调用WeatherbitService服务,并且把经纬度传递给它
  WeatherbitService.weatherDataForLocation(
    latitude: location.latitude,
    longitude: location.longitude) { [weak self] (weatherData, error) in
    //2 更新View
    guard let self = self, let weatherData = weatherData else { 
        return 
    }
    self.dateLabel.text = self.dateFormatter.string(from: weatherData.date)
    self.currentIcon.image = UIImage(named: weatherData.iconName)
    let temp = self.tempFormatter.string(from: weatherData.currentTemp as NSNumber) ?? "")
    self.currentSummaryLabel.text = "\(weatherData.description) - \(temp)℉"
    self.forecastSummary.text = "\nSummary: \(weatherData.description)"
  }
}

3.2 重构

3.2.1 使用Box类型来实现数据绑定

        在 MVVM中,需要一种将视图模型输出绑定到视图的方法。 为此,需要使用方式来实现此种功能:提供一种简单的机制来将视图绑定到视图模型的输出值。 有几种方法可以进行此类绑定:

  • KVO:一种使用键路径观察属性并在该属性更改时获取通知的机制。
  • Functional Reactive Programming(FRP):将事件和数据作为流处理的范式。 Apple 新的Combine框架是一种FRP方法; RxSwift 和 ReactiveSwift 是 FRP 的另外两个流行框架。
  • Delegation:使用代理方法在Model值发生变化时进行通知
  • Boxing:使用属性观察的方式在值发生变化时通知观察者
// 在Utilities文件夹创建Box.swift文件

final class Box<T> {
  // 定义一个通知方法的类型
  typealias Listener = (T) -> Void
  var listener: Listener?

  // 在值发生变化时通知观察者
  var value: T {
    didSet {
      listener?(value)
    }
  }

  // 使用值初始化此Box对象
  init(_ value: T) {
    self.value = value
  }

  // 在值和观察者之间建立绑定并通知观察者
  func bind(listener: Listener?) {
    self.listener = listener
    listener?(value)
  }
}

3.2.2 创建WeatherViewModel

        现在已经建立了在视图和视图模型之间进行数据绑定的机制,可以开始构建的视图模型。 在MVVM中,视图控制器不调用任何服务或操作任何模型类型,该责任完全由视图模型承担。
        通过把与地理编码器和 Weatherbit 服务相关的代码从 WeatherViewController 移动到 WeatherViewModel 来开始重构。 然后,将视图绑定到 WeatherViewController 中的视图模型属性。
        首先,在View Models文件夹创建WeatherViewModel.swift文件。

// 普通情况下不得在ViewModel里引用UIKit,此处需要使用图片,所以仅引入UIKit中的UIImage类型
import UIKit.UIImage
// 把此类型设置为public是为了能够进行单元测试
public class WeatherViewModel {
}

        第2步,修改WeatherViewController.swift。

// 在WeatherViewController.swift文件中添加一下属性
private let viewModel = WeatherViewModel()

        第3步,把LocationGeocoder的相关逻辑迁移到WeatherViewModel.swift。

// 把defaultAddress迁移到WeatherViewModel
// 把geocoder迁移到WeatherViewModel

// 在WeatherViewModel添加一个新属性
let locationName = Box("Loading...")

// 在WeatherViewModel中添加以下函数
func changeLocation(to newLocation: String) {
  locationName.value = "Loading..."
  geocoder.geocode(addressString: newLocation) { [weak self] locations in
    guard let self = self else { return }
    if let location = locations.first {
      self.locationName.value = location.name
      self.fetchWeatherForLocation(location)
      return
    }
  }
}

        第4步,修改WeatherViewController.swift。

// 把原来的viewDidLoad方法使用以下代码进行替换
override func viewDidLoad() {
  viewModel.locationName.bind { [weak self] locationName in
    self?.cityLabel.text = locationName
  }
}

        第5步,修改WeatherViewModel.swift。

// 添加初始化方法
init() {
  changeLocation(to: Self.defaultAddress)
}

// 添加获取数据的方法(目前方法为空)
private func fetchWeatherForLocation(_ location: Location) {
  WeatherbitService.weatherDataForLocation(
    latitude: location.latitude, 
    longitude: location.longitude) {
      [weak self] (weatherData, error) in
      guard let self = self, let weatherData = weatherData else { 
          return 
      }
  }
}

3.2.3 格式化数据

        在 MVVM 中,视图模型始终负责格式化来自服务和模型类型的数据以呈现在视图中。

// 第1步:把dateFormatter迁移到ViewModel中

// 第2步:在ViewModel中添加新的绑定
let date = Box(" ")

// 第3步:在ViewModel中WeatherViewModel.fetchWeatherForLocation(_:)函数的闭包的尾部添加以下代码
self.date.value = self.dateFormatter.string(from: weatherData.date)

// 第4步:在WeatherViewController.viewDidLoad()的尾部添加以下绑定
viewModel.date.bind { [weak self] date in
  self?.dateLabel.text = date
}

// 第5步:把tempFormatter从WeatherViewController迁移到WeatherViewModel

// 第6步:在WeatherViewModel添加以下绑定
let icon: Box<UIImage?> = Box(nil)  //no image initially
let summary = Box(" ") 
let forecastSummary = Box(" ")

// 第7步:在WeatherViewController.viewDidLoad()添加绑定代码
viewModel.icon.bind { [weak self] image in
  self?.currentIcon.image = image
}
    
viewModel.summary.bind { [weak self] summary in
  self?.currentSummaryLabel.text = summary
}
    
viewModel.forecastSummary.bind { [weak self] forecast in
  self?.forecastSummary.text = forecast
}

// 第8步:更新Box的值(WeatherViewModel.fetchWeatherForLocation(_:))
self.icon.value = UIImage(named: weatherData.iconName)
let temp = self.tempFormatter
  .string(from: weatherData.currentTemp as NSNumber) ?? ""
self.summary.value = "\(weatherData.description) - \(temp)℉"
self.forecastSummary.value = "\nSummary: \(weatherData.description)"

// 第9步:更新WeatherViewModel.changeLocation(to:)的代码
self.locationName.value = "Not found"
self.date.value = ""
self.icon.value = nil
self.summary.value = ""
self.forecastSummary.value = ""

3.3 添加功能

        目前,此App只能展示固定位置的天气信息,在本小节,我们将通过添加功能的方式来实现——切换不同地区都可以显示其对应天气的功能。


左上角箭头图片

        在此图的左上角箭头位置是一个按钮,添加其点击事件,并添加如下代码:

// 创建一个alert
let alert = UIAlertController(
  title: "Choose location",
  message: nil,
  preferredStyle: .alert)
alert.addTextField()
// 添加一个alertaction
let submitAction = UIAlertAction(
  title: "Submit", 
  style: .default) { [unowned alert, weak self] _ in
    // 更新位置,从而触发天气的更新
    guard let newLocation = alert.textFields?.first?.text else { return }
    self?.viewModel.changeLocation(to: newLocation)
}
alert.addAction(submitAction)
// 展示alert
present(alert, animated: true)

四、MVVM总结

        通过以上的简单实例,我们已经通过把MVC的模型转换为MVVM模型。从中我们可以看到MVVM具有以下优势:

  • 降低复杂性(减轻胖controller)的功能
  • 拆分业务逻辑:把不同的代码迁移到不同的文件中(把业务逻辑迁移到ViewModel中、净化ViewController)
  • 可维护(代码简单了、没有那么大模块了)
  • 可测试(ViewModel比ViewController好测试多了)

相关文章

  • 通过重构把MVC转变为MVVM

    一、引言 本文把一个MVC的工程[https://koenig-media.raywenderlich.com/u...

  • android提升大法

    1、架构设计 1.1 设计模式 1.2 重构《重构改善既有的代码设计》 1.3 架构模式MVP MVC MVVM ...

  • Android技术博文汇总

    架构 MVC,MVP 和 MVVM 的图示 浅谈 MVP in Android Android项目重构之路:架构篇...

  • React、Vue学习总结

    一、 从MVC到MVVM 从MVC到MVVM 1. (客户端)MVC调用关系 用户通过操作view调用contro...

  • iOS用被误解的MVC重构代码

    前言 这段时间在重构代码,看了几种模式,最后选择使用被误解的MVC来重构。下面分别简要介绍MVVM(RAC)、MV...

  • 为何放弃MVC使用MVVM

    为何放弃MVC使用MVVM 为何放弃MVC使用MVVM

  • iOS Review

    MVC,MVP,MVVM MVC:model和view不能直接通信,只能通过controllerMVP: 增加了p...

  • 前端MVC/MVVM模式特点及区别

    MVC,MVP,MVVM是三种常见的前端架构模式,它通过分离关注点来改进代码组织方式。MVC模式是MVP,MVVM...

  • iOS 设计模式 一

    设计模式随记 系统架构模式 1. MVC - MVVM - MVP - MVVM、MVC协调版 MVC :...

  • MVC - MVVM 是什么

    MVC - MVVM 是什么 谈谈MVC模式 - 阮一峰 MVC,MVP 和 MVVM 的图示 - 阮一峰 MVC...

网友评论

      本文标题:通过重构把MVC转变为MVVM

      本文链接:https://www.haomeiwen.com/subject/eebwultx.html