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如何来呢?

  1. 从VC_ViewModel中,赋予数据给View,在View内部自己生成ViewModel;
  2. VC_ViewModel中生成好View_ViewModel,然后View的初始化方法接收的是ViewModel。

这两种方法各有利弊吧:
第一种方法能够实现完全解耦,各自View除了View层面之外,其他没有一点联系;
第二种方法,对View层面不暴露数据,而且还将ViewModel串成了一个串,这样,对应View这个集合,ViewModel也自成一体,各个view的ViewModel之间也可以存在联系。而且,我也确实遇到过需要ViewModel间进行数据交互的情况,这种情况下,这种方式就会更显方便了。当然,这种方式中的耦合会不会有什么隐藏的弊端,目前还看不出什么。日后随着使用MVVM越来越多,相信能有更深切的体会吧~
所以现在还是请各位小伙伴自行选择吧~

那么model的生成时机呢?同理,就是在ViewModel的初始化方法最开始的地方。因为ViewModel的后续操作,也是都要以Model的数据为基础展开的。

3. 网络请求

网络请求我使用的是RACSubject

  1. 在发起网络请求时先新建一个RACSubject实例对象(以下简称subject),最终subject要作为方法返回值return出去;
  2. 然后在网络请求的回调中,使用subject进行对应的sendNext:sendCompletedsendError:操作;
  3. 外部直接订阅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;
}

注意:在每次网络请求的最后,都要以sendCompletedsendError:这类结束信号的动作来结束网络请求。
因为如果将 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]就可以了。

这里有两点需要单独提下:

  1. 这类RACSubject的实例对象,需要在viewModel初始化时同步初始化,不然后面在使用时是直接进行数据传递的,还未初始化不能使用;
  2. 视情况而定是否需要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_textSignalRACObserve(textField, text)两个信号merge到一起,才能实现对textField内容变化的完全监控。

6. RACObserve(TARGET, KEYPATH)的两种姿势

  1. 最常见的
RACObserve(model, name);

监控model的指定属性的变动。

  1. 如果model的直接变了呢?
    在某些时候,可能会需要通过刷新model对象来刷新展示层,再具体点,也就是声明一个model类型的属性,然后通过给该属性赋值,来整体刷新model。
    刚开始的时候,我理所当然的认为方法1的方式同样可以监听model.name的变化,但实际上并不然,那么此时应该如何监听呢?
RACObserve(self, currentModel.name);

这样的方式,就可以监听到对象整体变化时,其内部属性的值。

7. 避免重复订阅

重复订阅最显著的表现就是,操作被多次执行
每次的订阅(例如subscribeNext:等),都会将当前的订阅者放到一个表中,然后在signal内部发出value时,都会遍历存储订阅者的表,执行每一个订阅者相应的signal处理block,注意,这里存储订阅者的表是没有去重处理的,因此,一个对象可以以订阅者的身份多次被存入到这个表中。那么,如果此时一个对象重复订阅了多次一个signal,则当该signal有新value传输时,这个对象的signal处理block就会被执行多次,而一般这个block中放的就是我们自己的逻辑代码,所以这部分代码就会被执行多次,可能会造成很多稀奇古怪的问题。
那么这个问题应该如果解决呢?
首先要明确哪些操作会造成订阅,从而明确应该对哪些操作格外注意(关于造成订阅的问题可Google各位前辈“冷热信号”转换相关的文章)。
其次,具体在应该如何解决重复订阅的问题呢?个人目前总结出以下两点:

  1. 最基本的,对于实现订阅的代码,避免因为业务的实现而让其多次执行;
  2. 在合适的时机取消订阅。对于某些操作,可能由于参数的不同,每次的操作就是要重新进行订阅,那么此时就只能在合适的实际取消之前的订阅了。如何取消呢?
    在订阅方法中,都会有一个RACDisposable类型的返回值,那么使用该返回值执行dispose方法,就会取消该signal的订阅。

以上就是个人在RAC + MVVM的实践中总结出的一些东西,这一领域我也是首次涉足,感觉很有趣,像玩《帝国时代》一样,有种带兵打仗的感觉。其中内容难免有些疏漏和错误,请各位大手子不吝赐教!非常感谢!将来如果有新的体悟,也会不断更新。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容