前言
请预留足够的时间,您将看到大量的文字描述。但是相信我,您绝对值得花时间在这些文字描述上面。我已经尽了我最大所能来阐述关于MVC和MVVM如此这般设计的原因以及我们应该如何思考一些相关的问题
让我们从MVC开始
说起MVC,必须拿斯坦福大学公开课上的这幅图来说明,这可以说是最经典和最规范的MVC标准
所以看懂这张图,你就应该明白MVC在iOS中的实现思路了。
你一直在使用MVC的思想只是你可能没有察觉到
在我们着手探究MVC之前,有些东西是你应该了解的,虽然你可能已经了解了,但是这里还是要拿出来说一下,因为这些内容对于你理解MVC是如何运作的有很大的帮助。
几乎所有的App都只干这么一件事:将数据展示给用户看,并处理用户对界面的操作。
写过iOS App开发的开发者都知道我们大量的操作都是在controller中写布局代码再把数据显示到视图上面,然后实现一些专门处理用户操作的逻辑(比如按钮点击事件的实现)。
这里实际上你已经无形之中使用了MVC的思想了:一句话描述就是Controller负责将Model的数据用View显示出来,换句话说就是在Controller里面把Model的数据赋值给View,比如在controller中写self.label.text = self.data[@”title”],只是还没有刻意建一个Model类出来而已。
MVC是如何进行工作的
简(xiang)单(xi)解释一下MVC所代表的意思(网上随处可见)
M:Model,数据模型,比如我们人类有一双手,一双眼睛,一个脑袋,没有尾巴,这就是模型,Model定义了这个模块的数据模型。在代码中体现为数据管理者,Model负责对数据进行获取及存放。数据不可能凭空生成的,要么是从服务器上面获取到的数据,要么是本地数据库中的数据,也有可能是用户在UI上填写的表单即将上传到服务器上面存放,所以需要有数据来源。既然Model是数据管理者,则自然由它来负责获取数据。Controller不需要关心Model是如何拿到数据的,只管调用就行了。数据存放的地方是在Model,而使用数据的地方是在Controller,所以Model应该提供接口供controller访问其存放的数据(通常通过.h里面的只读属性)。
V:View,视图,简单来说,就是我们在界面上看见的一切。大多数情况都是继承自UIView的类的对象,而有时候则不是直接继承自UIView,有时候会直接用CoreAnimation甚至CoreGraphics来绘制内容,但这些东西统统都归结为MVC中的View。它们有一部分是我们UI定死的,也就是不会根据数据来更新显示的,比如一些Logo图片啊,这里有个按钮啊,那里有个输入框啊,一些显示特定内容的label啊等等;有一部分是会根据数据来显示内容的,比如tableView来显示好友列表啊,这个tableView的显示内容肯定是根据数据来显示的。我们使用MVC解决问题的时候,通常是解决这些根据数据来显示内容的视图。这里要提个醒:MVC虽然看似把模块分为了三部分,但是并不是说一个模块就要一定建三个类出来,比如联系人列表(ContactsList)模块,一定会有的肯定是ContactsListViewController,然后我们使用MVC来作为内部的框架,则需要创建ContactsListModel类,接下来会有少年继续创建ContactsListView,他认为这才是MVC,有三个类分别是XXXModel、XXXController、XXXView,这就大错特错了,你们在Controller中使用的UILabel、UITableView等等就是MVC中的View了,不要再专门建一个XXXXView出来,完全是没事找事多此一举。
C:Controller,最熟悉却又最抽象的一个东西。之所以熟悉,我们在使用UIKit进行开发中我敢说是你打交道最多的类:UIViewController。UIKit框架离不开UIViewController,当然你完全可以使用UIView来完成所有本该由Controller完成的事情,这是大逆不道的,因为这样做将会使你的整个项目代码一团糟,并且完美的违背了面向对象的思想:各司其职。这里大家一定要知道UIViewController究竟应该做些什么事,实际上就是API提供出来的接口:self.view用来作为所有视图的容器;管理自己的生命周期;controller之间的转场;controller容器。这是Controller的本职工作,然而它往往还需要承担起MVC中的数据和视图的协调者,也就是在Controller里面把Model的数据赋值给View来显示(或者是View接收用户输入的数据然后由Controller把这些数据传给Model来保存到本地或者上传到服务器)。
综合以上内容,实际上你应该可以通过面向对象的基本思想来推导出controller出现的原因:我们所有的App都是界面和数据的交互,所以需要类来进行界面的绘制,于是出现了View,需要类来管理数据于是出现了Model。我们设计的View应该能显示任意的内容比如UILabel显示的文字应该是任意的而不只是某个特定Model的内容,所以我们不应该在View的实现中去写和Model相关的任何代码,如果这样做了,那么View的可扩展性就相当低了。而Model只是负责处理数据的,它根本不知道数据到时候会拿去干啥,可能拿去作为算法噼里啪啦去了,可能拿去显示给用户了,它既然无法接收用户的交互,它就不应该去管和视图相关的任何信息,所以Model中不应该写任何View相关代码。然而我们的数据和界面应该同步,也就是一定要有个地方要把Model的数据赋值给View,而Model内部和View的内部都不可能去写这样的代码,所以只能新创造一个类出来了,取名为Controller。它被UIKit逐渐完善成了我们现在使用的UIViewController。
看图说话
你要问我以上这么一大堆东西我是怎么知道的?我就完成了一次看图说话而已,我们终于要开始讲解这张图了,为了更好的观看体验,我再次向您呈现出这张图,这样你就不需要往上面翻一大段内容了。
duang!
废话不多说了,这张图把MVC分为三个独立的区域,并且中间用了一些线来隔开。很有意思的设计,因为这些线似乎出现在了驾校科目一的内容中,你瞧C和V以及C和M之间的白线,一部分是虚线一部分是实线对吧,这就表明了引用关系:C可以直接引用V和M,而V和M不能直接引用C,至少你不能显式的在V和M的代码中去写和C相关的任何代码,而V和M之间则是双黄线,没错,它们俩谁也不能引用谁,你既不能在M里面写V,也不能在V里面写M。哦,上面的描述有点小小的问题,你不是“不能”这样写,而是“不应该”这样写,没人能阻止你在写代码的时候在一个M里面去写V,但是一旦你这样做了,那么你就违背了MVC的规范,你就不是在使用MVC了,所以这算是MVC的一个必要条件:使用MVC –> M里面没有V的代码。所以M里面没有V的代码就是使用MVC的必要条件,如果P->Q,那么~Q->~P,初中还是高中数学讲过吧,如果你在M里面写了V的代码,那么你就不是在使用MVC了。之前我见过什么字典转模型,然后把模型赋值给一个Cell在Cell内部解析模型来显示的,这我只能说你用的不是MVC。
它们三个分别是什么,干什么事,我在一开始就说清楚了。所以现在我们来看看它们如何进行交互,你可以理解为如何传值。
View和Controller的交互
iOS中的传值包括了事件的传递,比如按钮点击事件,是View来接收的,但是处理这个事件的应该是Controller,所以View把这个事件传递给了Controller,如何传递的呢,见图,看到View上面的action没有,这就是事件,看到Controller上面的target没有,这就是靶子,View究竟要把事件传递给谁,它被规定了传递给靶子,Controller实际上就是靶子,你们为按钮添加点击事件怎么写的?[按钮 添加靶子:我 事件:xxx …];只是View只负责传递事件,不负责关心靶子是谁。就像你是一个负责运货的少年,你唯一知道的是你要把货(action)交给上头(开发者)告诉你的那个收货的人(target),至于那个收货的人是警察还是怪兽,你都不需要关心。这是V和C的一种交互方式,叫做target-action。所以你看,这张图简直就是神来之笔,旁边还栩栩如生的画出了V对C的另一种传值:协议-委托。委托有两种:代理和数据源。什么是代理,就是专门处理should、will、did事件的委托,什么是数据源,就是专门处理data、count等等的委托。你们用的最多的,tableView用过吧,没用过还敢说你是做iOS开发的?你们有没有想过为什么tableView需要数据源来实现协议方法而不是直接把数据通过属性传给tableView?如果你来我这面试,恭喜你,你将会有幸被问到这个问题。
tableView并不像简单的视图那样显示简单的内容,它要显示的内容之丰富,它自己都不知道,它被设计为能显示任意多分组、每个分组任意多个单元格、每个单元格上面能显示任意内容、甚至每个单元格的高度都不一样等等,这样苛刻的条件,绝对不是一次简单的属性赋值就能解决的。然而这样类似的东西早在C语言库函数中就有了传说:我们牛X的排序函数。这个排序函数被设计为能为任意类型的数组进行排序,管你是整型数组还是字符串数组,还是你搞的奇奇怪怪的结构体数组,劳资都能排!没错,如果你学习过回调函数,那么你一定接触过这个牛X的排序函数的例子,对任意类型数组进行排序唯一的问题在于:数组中的元素的比较规则。实际上这个问题用代码来描述起来:排序函数S在排序的过程中,需要不停比较数组中某两个元素,S通过比较的结果来进行排序操作。意味着S只需要比较结果,至于如何比较,就由调用方提供的回调函数来决定,你让劳资帮你排序这个数组,你TM还不知道这个数组里面的东西如何比较,那我排个毛?所以问题解决了:S函数需要由调用方提供一个函数指针作为参数,这个函数指针指向的函数接收两个参数并返回这两个参数比较的结果,S只需要在需要比较的时候调用这个函数指针指向的函数,传入S想要比较的两个元素,拿到返回的比较结果就行了。
所以,明白了吧,tableView如何来实现那么苛刻的效果:我(tableView)在绘制的时候需要调用方提供方法给我,我只要结果不要过程!我先通过参数告诉你,我现在在画第1个section啦,你快告诉我这个section下面有多少个单元格?也就是调用[self.dataSource tableView:self numberOfRowsInSection:section]的返回值就是在section下有多少个单元格,tableView只需要不停的调用方法获取结果,绘制和数据处理的逻辑都在调用方(dataSource)。就像一个孩子一样,tableView一直在问问题,dataSource一直在回答tableView的问题,问题问完了,tableView就画出来了。tableView被这样设计出来以后,任意类都可以调用它,但是呢,调用tableView是有条件的,那就是你必须要有能解决我问题的能力,这就是协议的诞生:你要用我是吧,那你得能够回答上我的问题。所谓能回答上tableView的问题,也就是实现了tableView所声明的协议。
dataSource通过回调的形式,让绘制逻辑由dataSource控制(dataSource协议方法的实现)而绘制过程则由V来进行(dataSource协议方法的调用)。调用方V通过参数把值传给实现方dataSource,实现方通过返回值把值回传给调用方,这样V就通过不停的调用dataSource方法获取它所需要的绘制信息,最终绘制出界面。
往往V的dataSource都是一个C,而C在实现dataSource协议的时候是通过M里面的数据来实现的,这样就相当于由C把M间接地赋值给了V。
同样的,delegate协议也是一种回调,它处理的更多是一种事件,看那几个单词:should、will、did,都是一种询问的形式,我该不该怎样怎样,我将要怎样怎样啦,我已经怎样怎样啦… 当这样的询问需求发生了以后(比如scrollView将要被拖动的一瞬间(willBeginDraging…),scrollView停止减速的一瞬间(didEndDecelarating…)等等),V就会调用delegate相应的方法。比如tableView单元格的点击事件,是由V来直接接收到的(因为用户直接操作的对象都是V),而需要处理这个点击事件的地方应该在C,所以V应该通过某种方式告知C,有个Cell被点击啦(didSelectRowAtIndexPath…),并且还要能告知C,是哪个Cell(indexPath)被点击了,所以当cell被点击的时候 ,V就通过调用delegate实现的协议方法,这样点击事件的处理就相当于交给了delegate来实现了,并通过参数告知delegate这次是哪个cell被点击了。简单来说,就是V和它的delegate之间事前已经约定好了一个协议,一旦V将要、已经怎样怎样的时候,就按照协议实现的内容去做。所谓按照协议实现的内容去做,就是让delegate调用协议方法。这样就相当于,V将要、已经怎样怎样的时候,在delegate里面相应的实现的协议方法就会被调用。
以上就是V向C传值的设计,总结一下,就是主要通过三种方式:action-target用来负责传递特定的事件;dataSource-protocol用来通过回调的形式动态通过数据绘制界面;delegate-protocol提前约定了对一些事件的处理规则,当被规定的事件发生后,就按照协议的规定来进行处理。协议委托可以通过协议方法的参数由V向C传值。比如cell点击事件的协议方法,tableView通过indexPath参数告诉C是哪个cell被点击了。
Model和Controller的交互
接下来看看从MVC出生到现在为止争议比较大的,M和C的交互。
我们从M的作用开始说起。
M是干嘛的?上面说了,M就是数据管理者,你可以理解为它直接和数据库打交道。这里的数据库可能是本地的,也可能是服务器上的,M会从数据库获取数据,也可能把数据上传给数据库。M也将提供属性或者接口来供C访问其持有的数据。我们就拿一个简单的需求作为例子,假如我想在一个模块中显示一段文字,这段文字是从网上获取下来的。
那么使用MVC的话,在C中肯定需要一个UILabel(V)作为属性来显示这段文字,而这段文字由谁来获取呢,肯定是由M来获取了。而获取的地方在哪里呢?通常在C的生命周期里面,所以往往是在C的一个生命周期方法比如viewDidLoad里面调用M获取数据的方法来获取数据。现在问题来了,M获取数据的方法是异步的网络请求,网络请求结束后,C才应该用请求下来的数据重新赋值给V,现在的问题是,C如何知道网络请求结束了?
这里我们一定要换一种角度去思考,我们进一步考虑M和V之间的关系:它们应该是一种同步的关系,也就是,不管任何时刻,只要M的值发生改变,V的显示就应该发生改变(显示最新的M的内容)。所以我们可以关注M的值改变,而不用关心M的网络请求是否结束了。实际上C根本不知道M从哪去拿的数据,C的责任是负责把M最新的数据赋值给V。所以C应该关注的事件是:M的值是否发生了变化。
所以我们只需要解决“C如何知道M的值发生了变化”这个问题。
幸运的是在OC中有一种机制恰好就是来解决“一个对象想要关心另一个对象的属性是否发生了变化”的问题,它叫做KVO。(见图)
KVO叫做键值观察,它让一个对象作为观察者去观察另一个对象的由某个键值路径所代表的属性,一旦这个属性发生了变化,那么系统就会调用观察者的一个方法叫做observingValueForKeyPath:…。比如C想要在M的data属性发生改变后刷新界面,那么就只需要向M添加观察者C,观察路径为@”data”,这样就相当于对C来讲,一旦M.data发生了变化,那么C的observingValueForKeyPath方法就会被调用,就可以在这个方法的实现中写self.label.text = self.model.data;这样就实现了M和V的同步。
图上还标明了一个东西叫做Notification也就是通知,比如你想网络请求失败以后应该弹出提示框,或者自动登录打开App请求首页数据失败想要返回到登录页面重新登录,这样的操作肯定应该在C里面进行,所以M的网络请求一旦失败,就可以向C发送一个通知,来告诉C,网络请求失败啦,你自己看着办。
我之所以说这里有争议,是因为block的出现。Block的出现完美的解决了一些回调实现起来很麻烦的问题,block的回调相当方便简单。这里完全可以由C向M传一个block,M在网络请求结束后调用block。但是呢,这样做我