iOS MVC、MVP、MVVM的正确使用姿势

iOS使用RAC实现MVVM的正经姿势

从MVC到MVVM

前言

MVVM是微软于2005年开发出的一种软件架构设计模式,主要是为了在WPF和Silverlight中更简单的对UI实现事件驱动编程。在WPF和Silverlight中,通过MVVM成功的实现了UI布局和数据逻辑的剥离。虽然WPF和Silverlight最后都没有推广开来,但是还是让大家看到了MVVM设计模式的优秀之处。

我有幸在早年参加过Expression Blend的自动化测试工作,期间做了不少WPF和Silverlight的App,算是较早一批接触熟悉MVVM的天朝码农了。在iOS平台出现了可以优雅实现MVVM的RAC时,着实激动了一下。下面就让我们先从最早的MVC开始慢慢说起。

如果你想简单点直接看代码:Show you the code

MVC理想设计模式

MVC是一种比较古老软件架构设计模式,主旨是将代码分为UI、数据和控制逻辑三大部分:

18-A

一个UI交互的整体过程:View接受用户操作发送给Controller,Controller根据操作对数据进行修改,Controller接受数据修改的通知,并根据通知更新对应的UI。当然Controller可能有一些自有逻辑会修改数据或者更新UI,从属关系上来说View和Model都属于Controller。

MVC实例

这是我比较喜欢的一个实例,实现一个简单的登录界面。先罗列一下简单的需求:

  1. 用户名有效长度为4-16位,无效时对应文本框显示为红色底色,有效时文本框显示为绿色底色,无输入时显示为白色底色。
  2. 密码有效长度为8-16位,对应文本框底色逻辑与用户名文本框一致。
  3. 登陆按钮在用户名和密码均有效时可用,否则禁用。

18-B

为了让代码看起来不那么多,我使用xib来绘制了简单的UI并完成了IBOutlet和delegate等的绑定。

然后呢需要写的代码就是大概下面这样了:

18-C

这里的usernamepassword两个属性可以看作Model层,文本框和按钮的xib就是View层,VC主体代码就是Controller层。可以看到所有的Model修改逻辑和UI更新逻辑都是在Controller里一起完成的。(完整代码

MVC解决的问题和优缺点

  • 代码成功分化为UI、数据和控制逻辑三大部分。
  • 易于理解使用,普及成本低。
  • Controller拥有View和Model,几乎可以控制所有逻辑。
  • 细节不够明确,基本上不明确归属的代码全部会放在Controller层。
  • 和UI操作事件绑定较重,难以进行单元测试。

MVC实际使用状况

因为上一节中提到的3和4两点,很多代码都只能写在Controller层。还因为xib的特殊性,对多人协作十分不友好,导致大部分UI的布局和初始化代码要用代码实现,而这些代码写成单独的类也多有不便,导致本该出现在View层的代码也堆积在了Controller层。而且在iOS中,UIViewController和UIView本来就是一一对应的。这就导致了MVC从最早的Model-View-Controller最终一点点变成了Massive-View-Controller

18-D

MVP设计模式

所谓设计模式,就是软件设计过程中为了解决普遍性问题而提出的通用解决方案。MVP的出现就是为了解决MVC的Controller越来越臃肿的问题,进一步明确代码的分工:

18-E

这个图看上去和MVC很相似,但是这里的实虚线和MVC设计模式不同。所表示的意义为View层持有Presenter层,Presenter层持有Model层,View层并不可直接访问到Model层。整体的UI交互流程和MVC类似。

这么做的意义就在于真正意义上的将UI逻辑和数据逻辑隔离,而隔离之后就可以更方便的对数据逻辑部分进行单元测试,隔离的另一个好处就是解开了一部分的耦合。

MVP实例

接着刚刚的实例,我们在它的基础上继续进行修改。

首先我们需要定义一个Presenter,头文件内把所有可接受的用户操作和更新UI需要用的回调定义好:

18-F

Presenter的内部实现:

18-G

可以看到Presenter做的事情就是把原来Controller的逻辑控制相关代码抽离出来构建成一个单独的类。接下来看一看对应的Controller现在变成什么样:

18-H

现在Controller的代码变得更加清晰了:两个更新数据的调用,三个更新UI的调用,多了一些初始化Presenter的操作。

因为现在Presenter只包含逻辑,所以我们也较容易实现一个单元测试:

18-I

从结果可以看到Controller的代码转移了一部分到Presenter,MVP也成功把逻辑和UI代码分离了。(完整代码

MVP优缺点

  • UI布局和数据逻辑代码划分界限更明确。
  • 理解难度尚可,较容易推广。
  • 解决了Controller的臃肿问题。
  • Presenter-Model层可以进行单元测试。
  • 需要额外写大量接口定义和逻辑代码(或者自己实现KVO监视)。

MVVM设计模式

随着UI交互越来越复杂,MVP本身的一些缺点还是会暴露出来。

比如虽然是可以写单元测试,但是单元测试写起来还是有很多“啰嗦”的部分,需要模拟一些假的UI处理逻辑来进行结果的验证,即使用block写法这个部分的代码量也省不了太多。

所有的用户操作和更新UI的回调需要细细定义,随着交互越来越复杂,这些定义都要有很大一坨代码。

逻辑过于复杂的情况下,Present本身也会变得臃肿难以重用,代码也会变的更加难以阅读和维护。

这时候,MVVM出现了,为了解决以上大部分问题:

18-J

首先ViewModel-Model层和之前的Present-Model层一样,没有什么大的变化。View持有ViewModel,这个和MVP也一样。变化主要在两个方面:

  1. ViewModel相较于Present,不仅仅是个逻辑处理机,它附带了自己的状态,所以被才可以被称为“Model”。ViewModel也因为这个变的更加独立完整,我们更容易通过ViewModel的状态去进行单元测试。Presenter在没有设置回调的时候其实一直在做空运算而已,运算得到的值没有进行存储,下次必须重新运算。
  2. View不直接通过传递用户操作来控制ViewModel,ViewModel也不直接通过回调来修改View。对常用的数据和UI控件的事件&属性,MVVM框架的底层均进行了封装,使得我们可以进行数据绑定操作。简单来说我们可以用类似[viewModel.username bind:usernameTextField.text]类似的操作使得viewModel的属性和UI控件的属性相互绑定,其中一方修改的时候另一方直接自动做对应更改。这样的话我们就不用重复的书写很多回调操作,也不用处理一大堆UI控件的delegate事件

其实MVVM的精华小部分在ViewModel,更大部分就在数据绑定,甚至有很多人觉得应该称MVVM为MVB(Model-View-Binder)。

数据绑定引申出来的一个概念就是数据管道(转换器),这个和大家学的数字电路比较相似:

18-K

这里我们有ABC三个数据源和两个双输入的转换器,我们可以进行组合得出各种想要的结果(如上图),甚至于我们可以多次组合来完成更复杂的计算(如下图):

18-L

这里的转换器就带来了第三点改进:

  1. 基于数据绑定和数据管道,可以对运算逻辑进行拆分和重用,最大程度的使代码易读易维护

MVVM实例

还是接着刚刚的工程,首先要参照Reactive Cocoa的文档把RAC添加到工程里。

ViewModel的定义

然后我们首先要把Present改造成ViewModel:

19-A

这里可以看到作为ViewModel输出值的属性设置成了readonly,剩下的usernamepassword是输入值。

单元测试

值得一提的是软件工程中最好是测试驱动开发(TDD)而不是写完逻辑再补测试,所以我们先改好单元测试:

19-B

从单元测试也很容易看出来ViewModel现在足够独立并易于测试。

View层和ViewModel层的绑定

我们再看一眼现在Controller应该怎么写:

19-C

首先看到原来的一行loginButton初始化代码没有了,因为数据绑定是自动更新的,初次绑定就会初始化状态。

对ViewModel进行输入数据的绑定,不再需要写UITextFieldDelegate然后再传递事件,一行代码完成绑定。

同样将ViewModel的输出数据绑定到UI,不需要再实现对应的回调,一样一行代码完成绑定。

这就是MVVM设计模式在最理想的情况下,Controller里需要和ViewModel交互的所有代码内容。

数据管道(转换器)

现在来说说刚刚的ConvertInputStateToColor,它其实就是一个状态到颜色的转换器:

19-D

19-E

这里利用RACSignal的map方法做了一个映射,这就是我们的转换器。当然我们以后也可以实现别的转换器来进行方便的替换,比如实现一个仅在有效态显示绿色其他状态都显示白色的转换器。另外这个转换器如果写的更通用点,也可以被别的模块重复使用。

ViewModel的UI无关性/转换器组合的多样可能性

这里要提一下为什么ViewModel不直接提供颜色值的输出:

  1. ViewModel应该不关心具体的UI相关逻辑,只关心自己的逻辑正确和独立完整性。
  2. 易于进行单元测试,枚举当然比颜色值好检查点……
  3. 提供更为基础的状态,这样和不同的转换器组合会产生更多的可能性。

这里的可能性指什么呢?举个例子:出现了用户有输入内容时展示对应文本框清空按钮的新需求。这时候我们只需要完成一个新的转换器:InputStateEmpty时返回isHidden = YES;其余情况下返回isHidden = NO。然后把对应输出源通过转换器绑定到清空按钮的isHidden属性上即可。另外上一节提到的另一种颜色转换器,也是一种多样性的体现。

  1. 可以进行二次组合,用以计算输出值loginEnabled。(见下一节)

ViewModel的完整实现

19-F

需要把输出源对应的属性偷偷改成readwrite的先,不然不可写的话绑定的时候会跪。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340