第10章:模型-视图-视图模型模式

模型-视图-视图模型(MVVM)是一种结构化的设计模式,将对象分成三个不同的组。

这种模式听起来很熟悉吗?是的,它与模型-视图-控制器(MVC)非常相似。请注意,本页顶部的类图包括一个视图控制器;视图控制器在MVVM中确实存在,但它们的作用被最小化了。

在本章中,你将学习如何实现视图模型,并组织你的项目以包括它们。你将从一个简单的例子开始,了解视图模型的作用,然后你将采用一个MVC项目并将其重构为MVVM。

你什么时候应该使用它?

当你需要将模型转换为视图的另一种表示时,请使用这种模式。例如,你可以使用视图模型将Date转换成数据格式的String,将小数转换成货币格式的String,或者许多其他有用的转换。

这种模式对MVC特别有利。如果没有视图模型,你可能会把模型到视图的转换代码放在你的视图控制器中。然而,视图控制器已经做了很多事情:处理viewDidLoad和其他视图生命周期事件,通过IBActions处理视图回调以及其他一些任务。

这就导致了开发者戏称的 "MVC:大规模的视图控制器"。

如何避免视图控制器的过度构建?很简单--使用MVC以外的其他模式! MVVM是瘦身的好方法,它能使需要进行多次模型到视图转换的庞大的视图控制器变得更小。

playground实例

打开Starter目录下的IntermediateDesignPatterns.xcworkspace,然后打开MVVM页面。

对于这个例子,你将制作一个 "宠物视图",作为收养宠物的应用程序的一部分。在代码示例后添加以下内容。

在这里,你定义了一个名为 Pet 的模型。每只宠物都有名字、生日、稀有度和形象。你需要在视图上显示这些属性,但不能直接显示生日和稀有度。它们首先需要通过视图模型进行转换。

接下来,将以下代码添加到 Playground 的末尾:

以下是你在上面所做的:

  1. 首先,你创建了两个名为 pet 和 calendar 的私有属性,并在 init(pet:) 中进行了设置。

  2. 接下来,为 name 和 image 声明了两个计算属性,分别返回宠物的 name 和 image。这是你可以执行的最简单的转换:不加修改地返回一个值。如果你想更改设计为每只宠物的名称添加前缀,你可以通过在此处修改名称来轻松实现。

  3. 接下来,你将 ageText 声明为另一个计算属性,在这里你使用日历计算从今天开始到宠物生日之间的年差,并将其作为字符串返回,后跟“岁”。你将能够直接在视图上显示此值,而无需执行任何其他字符串格式化。

  4. 最后,你创建了 admissionFeeText 作为最终计算属性,你可以在其中根据宠物的稀有性确定宠物的收养成本。同样,你将其作为字符串返回,以便你可以直接显示它。

现在你需要一个 UIView 来显示宠物的信息。将以下代码添加到操场的末尾:

在这里,你创建了一个带有四个子视图的 PetView:一个用于显示宠物图像的 imageView 和三个用于显示宠物名称、年龄和收养费的标签。

你在 init(frame:) 中创建和定位每个视图。最后,你在 init?(coder:) 中抛出一个 fatalError 来表明它不被支持。

你已经准备好将这些班级付诸行动了! 在Playground的末尾添加以下代码。

以下是你的做法。

  1. 首先,你创建了一个名为stuart的新宠物。

  2. 接下来,你用stuart创建了一个viewModel。

  3. 接下来,你通过在iOS上传递一个普通的框架尺寸来创建一个视图。

  4. 接下来,你用viewModel配置了视图的子视图。

  5. 最后,你将视图设置为PlaygroundPage.current.liveView,这告诉Playground在标准的助理编辑器中渲染它。

要看这个动作,请选择Editor ▸ Live View来查看渲染的视图。

斯图尔特究竟是什么类型的宠物?当然,他是一只饼干怪兽。它们非常罕见。

你可以对这个例子做最后的改进。在PetViewModel的类关闭大括号后添加以下扩展。

你将用这个方法来配置视图,而不是用视图模型来做这个内联。

找到你之前输入的以下代码。

// 4 
view.nameLabel.text = viewModel.name 
view.imageView.image = viewModel.image 
view.ageLabel.text = viewModel.ageText 
view.adoptionFeeLabel.text = viewModel.adoptionFeeText

...并将该代码替换为以下内容。

viewModel.configure(view)

这是一个将所有的视图配置逻辑放入视图模型的好方法。在实践中,你可能想这样做,也可能不想。如果你只对一个视图使用视图模型,那么把configure方法放到视图模型中会很有用。

但是,如果你在使用视图模型的时候有一个以上的视图,那么你可能会发现把所有的逻辑都放在视图模型中会很麻烦。在这种情况下,为每个视图单独配置代码可能更简单。

你的输出应该和以前一样。

嘿,斯图尔特,你要分享那个饼干吗?不分享吗?噢,来吧...!

你应该注意什么?

如果你的应用程序需要许多模型到视图的转换,那么MVVM的效果很好。然而,不是每个对象都能整齐地归入模型、视图或视图模型的类别。相反,你应该将MVVM与其他设计模式结合起来使用。

此外,MVVM在你刚创建应用程序时可能不是很有用。MVC可能是一个更好的起点。当你的应用程序的要求发生变化时,你可能需要根据你不断变化的要求来选择不同的设计模式。当你真正需要它时,可以在应用程序的后期引入MVVM。

不要害怕变化--相反,要提前做好计划。

教程项目

在本节中,你将向一个名为Coffee Quest的应用程序添加功能。

在Starter目录下,在Xcode中打开CoffeeQuest ▸ CoffeeQuest.xcworkspace(不是.xcodeproj)。

这个应用程序显示由Yelp提供的附近的咖啡店。它使用CocoaPods来拉入YelpAPI,这是一个用于搜索Yelp的辅助库。如果你以前没有使用过CocoaPods,那也没关系!你所需要的一切都已经包含在其中了。你所需要的一切都已包含在启动项目中。你唯一需要记住的是打开CoffeeQuest.xcworkspace,而不是CoffeeQuest.xcodeproj文件。

注意:如果你想了解更多关于CocoaPods的信息,请阅读我们关于它的教程:http://bit.ly/cocoapods-tutorial。

在你运行该应用程序之前,你首先需要注册一个Yelp API密钥。

在你的网络浏览器中导航到这个URL。

如果你没有账户,请创建一个账户,或者登录。接下来,在 "创建应用程序 "表格中输入以下内容(如果你以前创建过一个应用程序,则使用你现有的API密钥)。

按 "创建新应用程序 "继续,你应该看到一个成功的消息。

复制你的API密钥并返回Xcode中的CoffeeQuest.xcworkspace。

打开CoffeeQuest\Resources\APIKeys.swift,将你的API密钥粘贴到指定位置。

模拟器的默认位置被设置为旧金山。哇,那座城市有很多咖啡馆啊

注意:你可以通过点击Debug ▸ Location,然后选择不同的选项来改变模拟器的位置。

这些地图针有点无聊。如果它们能显示出哪些咖啡店是真正好的,那不是很好吗?

打开CoffeeQuest\Models\MapPin.swift。MapPin需要一个坐标、标题和评级,然后将这些转换为地图视图可以显示的东西......这听起来很熟悉吗?是的,它实际上是一个视图模型

首先,你需要给这个类起一个更好的名字。在文件顶部的MapPin上点击右键,选择Refactor ▸ Rename。

这让其他开发者明显感觉到你在使用MVVM模式。明确性是好的!

BusinessMapViewModel需要更多的属性,以显示令人兴奋的地图注释,而不是由MapKit提供的普通图钉。

还是在BusinessMapViewModel.swift中,用以下内容替换import Foundation。

import UIKit

你在这里导入UIKit,因为你要添加需要UIKit的组件。

接下来,在现有的属性之后添加以下属性;暂时忽略由此产生的编译器错误。

public let image: UIImage 
public let ratingDescription: String

你将使用图像而不是默认的针状图像,并且当用户点击注释时,你将以副标题的形式显示评级描述。

接下来,把init(coordinate:name:rating:)替换成以下内容。

你通过这个初始化器接受图像,并从评级中设置ratingDescription。

接下来,在MKAnnotation扩展的末尾添加以下计算属性。

public var subtitle: String? { 
  return ratingDescription 
}

这告诉地图在选择注解呼出时使用评级描述作为副标题。现在你可以消除编译器的错误了。打开ViewController.swift,向下滚动到文件的末尾。

将addAnnotations()改为以下内容。

这个方法与之前类似,只是现在你要切换到评级(见//1)来决定使用哪个图片。高质量的咖啡因对开发者来说就像猫薄荷,所以你把任何低于3.5星的东西都标记为 "坏"。你得有高标准,对吗? ;]

建立并运行你的应用程序。它现在应该看起来......一样?怎么会这样?

地图并不了解图像。相反,你应该覆盖一个委托方法来提供自定义的针脚注释图像。这就是为什么它看起来和以前一样了。

在addAnnotations()之后添加以下方法。

这只是创建了一个MKAnnotationView,它为给定的注释显示正确的图像;这是你的BusinessMapViewModel对象之一。

构建并运行,你应该看到自定义的图像 点击一个,你会看到咖啡店的名字和评价。

看起来大多数旧金山的咖啡店实际上都是四星级或以上的,你现在可以一目了然地找到最好的商店。

关键点

你在本章中了解了模型-视图-视图模型(MVVM)模式。以下是它的关键点。

你为Coffee Quest添加了一个非常好的功能,按评级显示咖啡店!但是,仍然有很多问题需要解决。然而,你还可以用这个应用程序做很多事情。继续下一章,学习工厂模式,继续构建Coffee Quest。


上一章 目录 下一章