ReactiveCocoa + MVVM 个人实践心得
本文不涉及长篇大论的原理,不涉及ReactiveCocoa源码(暂时应该不会),因为有很多前辈的文章讲解的要比我好得多。这里只是讲述我在使用ReactiveCocoa(以下简称RAC)将项目重构成MVVM结构过程中,一些实践心得。
ReactiveCocoa版本:ReactiveObjC 3.1.0
1. 目录结构
“一千个码农心里有一千种目录结构”。
目录结构这个东西,真的不是很好拿出来说,因为这个东西受个人习惯影响较大,尤其是个人开发者或独立开发人员,而且没有绝对的对与错。这里之所以还要拿出来说,主要是为了如果转到MVVM,结构变动较大的,再直白点,就是之前MVC都用得不是那么标准的童鞋(例如我🤦🏻♂️...)。
我的工程中,根目录是使用业务模块区分的,这个看各位自己的习惯。然后细化到具体功能页面,这里我之前的习惯就是M、V、C三个个文件夹就完事了,由于Massive ViewController的问题在我身上体现的淋漓尽致,所以每个文件夹中的文件也就很少,比较容易区分;而转到MVVM之后,一个有个tableView的VC,光View文件夹中就可能有VC、TableView、Cell三个类,六个文件,然后ViewModel文件夹中对应数量的ViewModel,和Model文件夹中可能更多的model。刚开始的时候选起来还真就有点乱,尤其是业务名前缀较长的时候,如果再遇上当前文件夹层级目录比较深。。。我的天!简直爆炸!多么渴望有一台21:9的显示器啊!!!
所以,建议对于稍复杂的页面,可以在业务文件夹下,按view再区分一次,然后每个view文件夹下面再去区分VC&View、ViewModel、Model。例如下图:
这样,在开发过程中,一般情况下,同一时间只会较频繁的在一个View下面的三个子文件夹中来回切换,个人认为这样会比较清晰和高效。
这里单独提一下这个命名方法的问题,我看到过某些标准上写,驼峰法和下划线法不应该一起使用,但是我这里还是同时使用了,为什么呢。。。这么写是真的好看且舒服。。。各位小伙伴如果觉得不妥,可以按自己的习惯来~
2. ViewModel
2-1. 三者各自的作用
- View:完成一切展示层的构建\操作;
- Model:存储与View相关的所有数据;
- ViewModel:连接View与Model,将Model的数据处理成View能直接使用的形式,例如网络请求等;外部反馈数据的更新也在这里处理/反馈给model。总而言之,ViewModel让view层不需要动数据,直接用即可。
2-2. 生成时机
在view的初始化方法中就应该生成。因为很多view的呈现都是依托于数据的,所以作为数据来源的ViewModel,应该在初始化方法中,在构建UI之前就生产完毕。
还有一种情景,就是VC中view,此时VC的全部数据都归他的ViewModel管理,那么VC中view在初始化时,也是需要ViewModel的,那么此时,view的ViewModel如何来呢?
- 从VC_ViewModel中,赋予数据给View,在View内部自己生成ViewModel;
- VC_ViewModel中生成好View_ViewModel,然后View的初始化方法接收的是ViewModel。
这两种方法各有利弊吧:
第一种方法能够实现完全解耦,各自View除了View层面之外,其他没有一点联系;
第二种方法,对View层面不暴露数据,而且还将ViewModel串成了一个串,这样,对应View这个集合,ViewModel也自成一体,各个view的ViewModel之间也可以存在联系。而且,我也确实遇到过需要ViewModel间进行数据交互的情况,这种情况下,这种方式就会更显方便了。当然,这种方式中的耦合会不会有什么隐藏的弊端,目前还看不出什么。日后随着使用MVVM越来越多,相信能有更深切的体会吧~
所以现在还是请各位小伙伴自行选择吧~
那么model的生成时机呢?同理,就是在ViewModel的初始化方法最开始的地方。因为ViewModel的后续操作,也是都要以Model的数据为基础展开的。
3. 网络请求
网络请求我使用的是RACSubject。
- 在发起网络请求时先新建一个RACSubject实例对象(以下简称subject),最终subject要作为方法返回值return出去;
- 然后在网络请求的回调中,使用subject进行对应的
sendNext:
、sendCompleted
或sendError:
操作; - 外部直接订阅return的subject,接收其send的数据。
这样,以这个RACSubject的instance object作为媒介,来传输网络数据的处理网络请求的方式就实现了。
主体代码如下:
-(RACSubject *)RAC_RequestWithMethod:(RequestMethod)requestMethod andUrlStr:(NSString *)urlStr andParameters:(id)parameters{
RACSubject *subject = [RACSubject subject];
//成功调用的block
void (^success)(NSURLSessionDataTask *dataTask, id responseObject) = ^(NSURLSessionDataTask *dataTask,id responseObject)
{
//所需的个性化处理...
[subject sendNext:responseObject];
[subject sendCompleted];//无论何种情况,网络请求结束后,都要sendCompleted,不然btn.rac_command显示为未完成,btn是不可点击状态
};
//失败调用的block
void (^failure)(NSURLSessionDataTask *dataTask,NSError *error) = ^(NSURLSessionDataTask *dataTask,NSError *error)
{
//所需的个性化处理...
[subject sendCompleted];
};
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
if (requestMethod == GET) {
[manager GET:urlStr parameters:parameters progress:nil success:success failure:failure];
} else {
[manager POST:urlStr parameters:parameters progress:nil success:success failure:failure];
}
return subject;
}
注意:在每次网络请求的最后,都要以sendCompleted
或sendError:
这类结束信号的动作来结束网络请求。
因为如果将 button.rac_command 和 另一个以网络请求的subject作为return signal的RACCommand 相绑定(这是一种很常见的操作),那么如果subject不结束,button的可交互状态就一直为否,因为这种绑定会默认在RACCommand执行中将button的可交互状态置为否,RACCommand执行结束后再自动置为是。除此之外,RACCommand的executing属性,一种用来判断RACCommand是否是执行中的属性,也会一直处于执行中的状态,无法达到真实效果。
4. Signal的结果处理
这一小段,其实用一个问句来描述更为恰当:“发起一个signal的地方,这个signal后续产生的所有数据\逻辑处理,都要回到这个signal进行处理吗?”
常规来说,就是这样的。一个操作产生了一个signal,后续产生的数据也就一直依托于这个signal进行传递,也就回到了操作发起的地方,方便处理。
但是个人更喜欢用具体的业务signal来承接、处理产生的数据,尤其是复用较多的操作。
举个例子:
btn1会触发login的操作,btn2会触发wechatLogin的操作,但是两种login的操作最终在某些特定的条件下,都会直接触发pushToHomeVC的UI层操作。pushToHomeVC的前序处理都是在viewModel里完成的,到了真正需要进行push操作的时候,就需要view层来进行处理了。而这里我不选择让数据原路返回各自的btn,而是在viewModel再声明一个public的
@property (nonatomic, readonly, strong) RACSubject *pushToHomeVCSignal;
这样,view层全部的pushToHomeVC的操作,就全部可以通过监听这个signal来完成,viewModel中所有与pushToHomeVC相关的数据,也都可以通过这个signal传递出去。
再举个更常用的例子:alertSignal
...都懂,对吧~
本应该原路返回的数据被截胡了,那原本用户交互产生的signal如何处理呢?直接return一个[RACSignal empty]
就可以了。
这里有两点需要单独提下:
- 这类RACSubject的实例对象,需要在viewModel初始化时同步初始化,不然后面在使用时是直接进行数据传递的,还未初始化不能使用;
- 视情况而定是否需要
sendCompleted
:对于某些需要反复使用的此类业务signal,例如alertSignal
,不要每次传递完数据后,都习惯性的再sendCompleted
,这样会造成下次再使用时无效,因为sendCompleted
此类结束性语句,会使signal的数据传输通道关闭。
而且,一个signal即使不执行sendCompleted
之类结束性语句,也不会影响当前各对象(V、VM、M)的释放。
而且对于某些不需要数据传递的操作,而且不会二次使用的,其实直接使用sendCompleted
都可以,然后订阅处,直接处理subscribeCompleted:
即可。
5. UITextField内容的《完全》监控
几乎在所有RAC的教程中,最先出现的示例代码都是对textField.rac_textSignal
的监控,来形象的说明RAC中数据的传递。但在实际应用中,rac_textSignal真的能“包治百病”吗?
其实并不然,因为textField的直接赋值(textField.text = @"xxx")并不能被rac_textSignal监控到,所以需要使用RACObserve(textField, text)
来直接监听textField.text的变化;而且有意思的是,手动输入,也不能触发rac_textSignal,所以需要将textField.rac_textSignal
和RACObserve(textField, text)
两个信号merge到一起,才能实现对textField内容变化的完全监控。
6. RACObserve(TARGET, KEYPATH)的两种姿势
- 最常见的
RACObserve(model, name);
监控model的指定属性的变动。
- 如果model的直接变了呢?
在某些时候,可能会需要通过刷新model对象来刷新展示层,再具体点,也就是声明一个model类型的属性,然后通过给该属性赋值,来整体刷新model。
刚开始的时候,我理所当然的认为方法1的方式同样可以监听model.name的变化,但实际上并不然,那么此时应该如何监听呢?
RACObserve(self, currentModel.name);
这样的方式,就可以监听到对象整体变化时,其内部属性的值。
7. 避免重复订阅
重复订阅最显著的表现就是,操作被多次执行。
每次的订阅(例如subscribeNext:
等),都会将当前的订阅者放到一个表中,然后在signal内部发出value时,都会遍历存储订阅者的表,执行每一个订阅者相应的signal处理block,注意,这里存储订阅者的表是没有去重处理的,因此,一个对象可以以订阅者的身份多次被存入到这个表中。那么,如果此时一个对象重复订阅了多次一个signal,则当该signal有新value传输时,这个对象的signal处理block就会被执行多次,而一般这个block中放的就是我们自己的逻辑代码,所以这部分代码就会被执行多次,可能会造成很多稀奇古怪的问题。
那么这个问题应该如果解决呢?
首先要明确哪些操作会造成订阅,从而明确应该对哪些操作格外注意(关于造成订阅的问题可Google各位前辈“冷热信号”转换相关的文章)。
其次,具体在应该如何解决重复订阅的问题呢?个人目前总结出以下两点:
- 最基本的,对于实现订阅的代码,避免因为业务的实现而让其多次执行;
- 在合适的时机取消订阅。对于某些操作,可能由于参数的不同,每次的操作就是要重新进行订阅,那么此时就只能在合适的实际取消之前的订阅了。如何取消呢?
在订阅方法中,都会有一个RACDisposable
类型的返回值,那么使用该返回值执行dispose
方法,就会取消该signal的订阅。
以上就是个人在RAC + MVVM的实践中总结出的一些东西,这一领域我也是首次涉足,感觉很有趣,像玩《帝国时代》一样,有种带兵打仗的感觉。其中内容难免有些疏漏和错误,请各位大手子不吝赐教!非常感谢!将来如果有新的体悟,也会不断更新。