ModelModelModelModel是一种规划形式(MV应用程序视图)在iOS应用程序视图获取了广泛的重视。它涉及到一个广泛的重视点。伴侣目标。

iOS开发—MVVM 教程:从 MVC 重构

如上所示,MVVM 形式由多层组成:

  • 模型:应用程序运转的应用程序数据。
  • View:用户界面的视觉元素。在 iOS 中,视图控制器与视图的概念是分不开的。
  • ViewModel:从视图输入更新模型并从模型输出更新视图。

MVVM 供给了一些优于Model-View-ControllerMVC的优势,这是 iOS 中事实上的办法:

  • 下降复杂性:MVVM 经过将大量事务逻辑移出视图控制器使视图控制器更简略。
  • 表达性:视图模型更好地表达视图的事务逻辑。
  • 可测验性:视图模型比视图控制器更简略测验。您终究能够测验事务逻辑,而不必担心视图完结。

在本教程中,您将经过将气候应用程序的架构从 MVC 更改为 MVVM 来重构气候应用程序。首要,您会将一切与气候和方位相关的逻辑从视图控制器移动到视图模型中。然后,您将为视图模型编写单元测验,以了解怎么轻松地将测验集成到您的新视图模型中。

在本教程结束时,您的应用程序应该答应您按称号挑选任何方位并检查该方位的气候摘要。

入门

首要运用本教程顶部或底部的“下载材料”按钮下载项目材料。然后,翻开开端项目。

该应用程序从 weatherbit.io 获取最新气候信息,并供给当时气候摘要。

要运用 Wea​​therbit API,您需求注册一个免费的 API 密钥。在您增加自己的 Weatherbit API 密钥之前,该应用程序将无法运转。前往www.weatherbit.io/account/cre…注册您的密钥。

取得 API 密钥后,回来 Xcode。

Services下,翻开WeatherbitService.swift。然后用您的新密钥替换apiKey的值。

iOS开发—MVVM 教程:从 MVC 重构

构建并运转。

iOS开发—MVVM 教程:从 MVC 重构

您应该看到 McGaheysville, VA 的气候和今日的日期。

介绍 MVVM 角色和责任

在深化重构之前,您有必要了解视图模型和视图控制器在 MVVM 形式中的效果。

视图控制器仅担任更改视图并将视图输入传递给视图模型。因而,您将从视图控制器中删去任何其他逻辑并将其移至视图模型。

相比之下,视图模型担任以下工作:

  • 模型输入:获取视图输入并更新模型。
  • 模型输出:将模型输出传递给视图控制器。
  • Formatting:格局化模型数据以供视图控制器显现。

了解现有的应用程序结构

留意:本节是对应用程序结构的可选检查。假如您现已了解 MVC 视图控制器并想开端重构,您能够跳到运用 Box 进行数据绑定。

了解当时 MVC 规划中的应用程序。首要,翻开项目导航器,如下所示:

iOS开发—MVVM 教程:从 MVC 重构

Controllers下,您会找到WeatherViewController.swift。这是您将重构以删去对模型和服务类型的任何运用的视图控制器。

Models下,您会发现两个不同的模型目标:WeatherbitDataLocation.WeatherbitData是一个结构,表示 Weatherbit API 回来的数据。是 Apple服务回来Location的方位数据的简化结构。CLLocation

服务包括WeatherbitService.swiftLocationGeocoder.swift。望文生义,WeatherbitService从 Weatherbit API 获取气候数据。LocationGeocoder将字符串转换为Location.

Storyboards包括LaunchScreenWeather故事板。

实用程序视图模型都是空的。您将在重构期间为这些组创立文件。

气候视图控制器

重构时,您将首要重视WeatherViewController.要了解WeatherViewController,首要要检查它的私有特点。

// 1
私家让 geocoder =  LocationGeocoder ()
 // 2
私家 让defaultAddress =  "McGaheysville, VA" 
// 3
私家让 dateFormatter: DateFormatter  = {
  让 dateFormatter =  DateFormatter ()
  dateFormatter.dateFormat =  "EEEE, MMM d"
  回来日期程序格局
}()
// 4
个人让 tempFormatter: NumberFormatter  = {
  让 tempFormatter =  NumberFormatter ()
  tempFormatter.numberStyle = .none
  回来 tempFormatter
}()
  1. geocoder承受String比如华盛顿特区之类的输入并将其转换为发送给气候服务的纬度和经度。
  2. defaultAddress设置默许地址。
  3. DateFormatter格局化日期显现。
  4. 终究,NumberFormatter有助于将温度呈现为整数值。

现在,看看viewDidLoad()

掩盖func  viewDidLoad () {
  geocoder.geocode(addressString: defaultAddress) { [弱 自我] 方位在
    保镳
      让自我=自我,
      让 location = locations.first
      别说{
        回来
      }
    self .cityLabel.text = location.name
     self .fetchWeatherForLocation(方位)
  }
}

viewDidLoad()调用geocoder转换defaultAddressLocation.回调运用回来方位来填充cityLabel的文本。然后,它location传入fetchWeatherForLocation(_:).

的终究一部分WeatherViewControllerfetchWeatherForLocation(_:)

func  fetchWeatherForLocation ( _  location : Location ) {
   //1 
  WeatherbitService .weatherDataForLocation(
    纬度:方位.纬度,
    经度:location.longitude) { [ weak  self ] (weatherData, error) in 
    //2
    保镳
      让自我=自我,
      气候数据=气候数据
      别说{
        回来
      }
    self .dateLabel.text = 
      self .dateFormatter.string(来自:weatherData.date)
     self .currentIcon.image =  UIImage(命名:weatherData .iconName)
    让 temp =  self .tempFormatter.string(
      来自:weatherData.currentTemp 作为NSNumber ) ?? “”
     self .currentSummaryLabel.text = 
      " \(weatherData.description) - \(temp) ℉" 
    self .forecastSummary.text =  " \n摘要:\(weatherData.description) "
  }
}

这个办法只做两件事:

  1. 调用气候服务并将方位的纬度和经度传递给它。
  2. 运用气候服务回调供给的气候数据更新视图。

现在您现已对现有的应用程序结构有了深化的了解,是时分开端重构了。

运用 Box 进行数据绑定

在 MVVM 中,您需求一种将视图模型输出绑定到视图的办法。为此,您需求一个实用程序,该实用程序供给一种简略的机制来将视图绑定到视图模型的输出值。有几种办法能够进行此类绑定:

  • Key-Value Observing 或 KVO:一种运用键路径来调查特点并在该特点更改时获取告诉的机制。
  • 功能反应式编程或 FRP:将事情和数据作为流处理的典范。Apple 新的 Combine 结构是其处理 FRP 的办法。RxSwift 和 ReactiveSwift 是 FRP 的两个流行结构。
  • 托付:运用托付办法在值更改时传递告诉。
  • 装箱:运用特点调查者告诉调查者值已更改。

在本教程中,您将运用拳击。对于简略的应用程序,装箱的自定义完结将捉襟见肘。

Utilities下,创立一个新的Swift文件。将其命名为Box。然后,将以下代码增加到文件中:

终究类Box < T >
   //1 
  typealias  Listener  = ( T ) -> Void 
  var listener:监听器?
   //2个
  参数值:T {
    设置{
      听吗?(价值)
    }
  }
  //3
  初始化(_值:T) {
    自我价值=价值
  }
  //4 
  func 绑定(监听器:监听器) {
     self .listener =监听器
    听吗?(价值)
  }
}

以下是上面代码的效果:

  1. 每个都Box能够有一个Listener告诉Box值何时更改。
  2. Box具有泛型类型值。特点调查器didSet检测任何更改并告诉Listener任何值更新。
  3. 初始化器设置Box的初始值。
  4. 当 aListener调用bind(listener:)Box,它会Listener立即收到有关Box‘ 的当时值的告诉。

创立 WeatherViewModel

现在您现已设置了在视图和视图模型之间进行数据绑定的机制,您能够开端构建您的实际视图模型。在 MVVM 中,视图控制器不调用任何服务或操作任何模型类型。该责任完全属于视图模型。

您将经过将与地舆编码器和 Weatherbit 服务相关的代码WeatherViewControllerWeatherViewModel.然后,将视图绑定到WeatherViewController.

首要,在View Models下,创立一个名为WeatherViewModel的新**Swift文件。然后,增加以下代码:

// 1
导入UIKit . UIImage 
// 2
公共类WeatherViewModel {
}

下面是代码分化:

  1. 首要,为UIKit.UIImage.UIKit视图模型中不需求答应其他类型。一般的经验法则是永远不要UIKit在您的视图模型中导入。
  2. 然后,将WeatherViewModel的类修饰符设置为public。您将其公开,以便对其进行测验。

现在,翻开WeatherViewController.swift。增加以下特点:

viewModel =  WeatherView (气候模型)

在这儿,您初始化控制器内的视图模型。

接下来,您将WeatherViewController‘ 的LocationGeocoder逻辑移动到WeatherViewModel.在您完结以下一切过程之前,该应用程序不会再次编译:

  1. 先剪defaultAddress下来WeatherViewController粘贴进去WeatherViewModel。然后,为特点增加一个静态修饰符。
  2. 接下来,剪下geocoder并将WeatherViewController其粘贴到WeatherViewModel.

WeatherViewModel中,增加一个新特点:

let locationName =  Box ( "加载中..." )

上面的代码将使应用程序在发动时显现*“正在加载…”*,直到获取方位。

接下来,将以下办法增加到WeatherViewModel

func  changeLocation ( to  newLocation : String ) {
  locationName.value =  "加载中..." 
  geocoder.geocode(addressString: newLocation) { [ weak  self ] 方位在
    守卫让自我=自我否则{回来}
    假如让方位=方位第一个{
       self .locationName.value = location.name
       self .fetchWeatherForLocation(方位)
      回来
    }
  }
}

此代码在经过 获取之前更改locationName.value为*“正在加载…”*geocoder。完结geocoder查找后,您将更新方位称号并获取该方位的气候信息。

替换WeatherViewController.viewDidLoad()为以下代码:

掩盖func  viewDidLoad () {
  viewModel.locationName.bind { [ weak  self ] locationName in 
    self ? .cityLabel.text =方位称号
  }
}

此代码绑定cityLabel.textviewModel.locationName.

接下来,在WeatherViewController.swift 中删去fetchWeatherForLocation(_:)

因为您依然需求一种办法来获取某个方位的气候数据,因而fetchWeatherForLocation(_:)WeatherViewModel.swift中增加一个重构:

个人func  fetchWeatherForLocation(_ 方位:方位){
   WeatherbitService .weatherDataForLocation (
    纬度:方位.纬度,
    经度:location.longitude) { [ weak  self ] (weatherData, error) in
      保镳
        让自我=自我,
        气候数据=气候数据
        别说{
          回来
        }
  }
}

您现在什么都不做,但将在下一节中完结这个办法。

终究,增加一个初始化器WeatherViewModel

在里面(){
  更改方位(到:Self .defaultAddress)
}

视图模型首要将方位设置为默许地址。

呸那是很复杂的。您将一切服务和地舆编码器从逻辑视图到视图模型。请留意视图是怎么移动的,同时也能够显现一切的视图。

要检查您当时的更改值,defaultAddress为您当时的更改方位。

构建并运转。

iOS开发—MVVM 教程:从 MVC 重构

检查城市称号现在您当时的方位。可是板子显现的日期和日期不来自故事。该应用程序正在显现正确的示例信息。

你会处理这个问题。

在 MVVM 中数据

在 MV 中视图和视图类型的数据以呈现在视图中的视图和视图类型只担任。

在下一次绑定时,您将在更新时,将您输入的数据格局以数据格局出出并WeatherViewController移至它WeatherViewModel时,您将增加一切剩下的数据,以便在方位更改时运用。

首要处理日期格局问题。首要,dateFormatterWeatherViewController。将特点贴到WeatherViewModel

,在中WeatherViewModel,增加以下内容locationName

让日期=框(“”)

它开始是一个空白字符串,并在气候数据从 Weatherbit API 抵达时更新。

WeatherViewModel.fetchWeatherForLocation(_:)现在,在 API fetch 闭包结束之前增加以下内容:

self .date.value =  self .dateFormatter.string(来自:weatherData.date

date每逢气候数据抵达的时分,上面的代码就会更新。

将以下代码附加到终究WeatherViewController.viewDidLoad()

viewModel.date.bind { [弱 自我]自我中的日期
  ?.dateLabel.text =日期
}

构建并运转。

iOS开发—MVVM 教程:从 MVC 重构

现在的日期实际上是今日的日期,而不是故事板中的 11 月 13 日。你在进步!

是时分完结重构了。按照这些终究的过程完结其他的字段需求的数据绑定。

首要,tempFormatterWeatherViewController.将特点粘贴到WeatherViewModel

然后,为剩下的可绑定特点增加以下代码WeatherViewModel

let icon: Box < UIImage ?> =  Box ( nil )   //开始没有图画
let summary =  Box ( " " ) 
 let forecastSummary =  Box ( " " )

现在,将以下代码增加到结尾WeatherViewController.viewDidLoad()

viewModel.icon.bind { [ weak  self ] image in
  self ? .currentIcon.image =图画
}
viewModel.summary.bind { [弱自我 ]自我
  总结?.currentSummaryLabel.text =摘要
}
viewModel.forecastSummary.bind { [弱自我 ]自我
  预测?.forecastSummary.text =预测
}

在这儿,您现已为图标图画、气候摘要和预告摘要创立了绑定。每逢框内的值发生变化时,视图控制器将自动得到告诉。

接下来,是时分实际更改这些Box目标中的值了。在WeatherViewModel.swift中,将以下代码增加到完结闭包的结尾fetchWeatherForLocation(_:)

self .icon.value =  UIImage (named: weatherData.iconName)
 let temp =  self .tempFormatter
  .string(来自:weatherData.currentTemp as  NSNumber)?? "" 
self .summary.value =  " \(weatherData.description) - \(temp) ℉" 
self .forecastSummary.value =  " \n摘要:\(weatherData.description) "

此代码格局化不同的气候项目以供视图显现。

changeLocation(to:)终究,在 API fetch 闭包的结尾和之前增加以下代码:

self .locationName.value =  "未找到"
self .date.value =  "" 
self .icon.value =  nil 
self .summary.value =  "" 
self .forecastSummary.value =  ""

假如地舆编码调用未回来任何方位,此代码可保证不显现气候数据。

构建并运转。

iOS开发—MVVM 教程:从 MVC 重构

现在,您的一切气候信息都会更新defaultAddress。假如您运用过当时方位,请检查窗外并承认数据正确。:] 接下来,您将看到 MVVM 怎么扩展应用程序的功能。

在 MVVM 中增加功能

到目前为止,您能够检查默许方位的气候。可是,假如您想知道其他地方的气候怎么办?您能够运用 MVVM 增加一个按钮来检查其他方位的气候。

您或许现已留意到左上角的方位符号 ➤。这是一个不起效果的按钮。接下来,您将把它与提示新方位的警报挂钩,然后获取该新方位的气候。

首要,翻开Weather.storyboard。然后,在帮手编辑器中翻开WeatherViewController.swift 。

接下来,按住 Control 并拖动Change Location ButtonWeatherViewController.将办法命名为promptForLocation

现在将以下代码增加到promptForLocation(_:)

//1
让警报=  UIAlertController (
  title: "挑选方位" ,
  音讯:无,
  首选款式:.alert)
alert.addTextField()
//2
让submitAction =  UIAlertAction (
  标题:“提交”,
  款式:.default) { [ unowned alert, weak  self ] _  in 
    guard  let newLocation = alert.textFields ? .first ? .text else {回来}
    自我?.viewModel.changeLocation(到:newLocation)
}
alert.addAction(submitAction)
//3
存在(警报,动画:真)

以下是此办法的细分:

  1. UIAlertController运用文本字段创立一个。
  2. 为提交增加一个操作按钮。该操作将新的方位字符串传递给viewModel.changeLocation(to:)
  3. 再现alert

构建并运转。

iOS开发—MVVM 教程:从 MVC 重构

您甚至能够尝试在法国巴黎的一些程序或您的巴黎寻觅一些不同的方位。

花点时刻想一下在视图中增加这个新功能需求多少代码。对视图模型的一次调用会触发更新方位的气候数据的流程。聪明,对吧?

,您将怎么运用 MV 创立单元创立学习测验。

运用 MVVM 进行单元测验

MVVM 的一大优势是它使创立自动化测验十分简略。

要运用 MVC 测验视图控制器,您有必要运用UIKit实例化视图。然后,您有必要查找视图层次结构以触发操作并验证成果。

运用 MVVM,您能够更经常地编写测验。您或许还需求等候一些事情,但大多数情况很简略触发和验证。

MVVM 使测验图模型需求很简略WeatherViewModel承认创立一个测验的方位,来更改您的绑定方位,然后locationName检查更新到您的方位。

首要,在MVVMFromMVCTests单元下,创立一个名为WeatherViewModelTests的新**测验用例类文件。

您有必要导入应用程序测验。紧接在下面进行import XCTest增加以下内容:

@testable导入Grados

现在,将以下办法增加到WeatherViewModelTests

func  testChangeLocationUpdatesLocationName () {
   // 1
  让希望=  self .expectation(
    描绘:“运用地舆编码器查找方位”)
  // 2
  让 viewModel =  WeatherViewModel ()
   // 3
  viewModel.locationName.bind {
    假如 $0 .caseInsensitiveCompare(“弗吉尼亚州里士满”)== .orderedSame {
      希望.fulfill()
    }
  }
  // 4 
  DispatchQueue .main.asyncAfter(deadline: .now() +  2 ) {
    viewModel.changeLocation(到:“弗吉尼亚州里士满”)
  }
  // 5 
  waitForExpectations(目标:8,处理程序:无)
}

下面是对新测验的解释:

  1. locationName异步异步的。运用expectation异步异步事情。

  2. 创立一个实例viewModel进行测验。

  3. locationName假如成果与预期称号成果匹配,则并满意预期。 “显现任何预期值,*正在显现…”*或默许地址。只要成果的成果满意测验加载的预期。

  4. 经过更改才能在更改之前等候发动geocoder几个测验。

    当它检查实例时,它也会触发geocoder款式。等候创立的其他测验查找在触发测验之前完结。

    Apple 的文档清晰正告说,CLLocation请求率太高,假如或许会过错表示。

  5. 最多只要几秒的成果,在抵达之前 8 时,才会测验成功。

点击旁边的菱形运转形testChangeLocationUpdatesLocationName()测验。测验经过后,菱形将变为复选标记。

iOS开发—MVVM 教程:从 MVC 重构

从这儿,您能够按照此示例创立测验来承认WeatherViewModel。理想情况下,将注入一个模拟气候服务来消除对 weatherbit.io 的依赖以进行测验。

重构到 MVVM

干得好,能够到这一步!当您回忆这些更改时,您能够看到重构带来一些 MVVM 的优点:

  • 简化复杂性WeatherViewController现在简略简略。
  • 专业化WeatherViewController不再依赖于任何模型类型,只重视视图。
  • 示例发送输入(例如发送输入(例如发送输入),或经过发送到其输出)与发送到。WeatherViewController``WeatherViewModel``changeLocation(to:)
  • 表达性:WeatherViewModel事务逻辑与低级看法不同。
  • 可维护性:经过对WeatherViewController
  • 可测验WeatherViewModel相对简略测验。

您应该考虑对 MVVM 进行一些逗留:

  • 其他类型:MVVM 为应用程序的结构引进了其他的视图模型类型。
  • 绑定机制:它需求一些数据绑定的手法,在这种情况下是Box类型。
  • 样板:你需求一些额外的样板来完结 MVVM。
  • 内存:在将视图模型引进组合时,您有必要留意内存管理和内存保存周期。

本教程的终究项目材料下载地址

链接:pan.baidu.com/s/1wxC7LIcG…

提取码:n3k7

这儿也引荐一些面试相关的内容!

  • ① BAT等各个大厂iOS面试真题+答案大全

  • ② iOS中高级开发必看的热门书本(经典必看)

  • ③ iOS开发高级面试”简历制造”辅导

  • ④ iOS面试流程到基础知识大全