MVVM设计模式加RAC响应式编程

一:为何要用MVVM?


为何要用MVVM?只是由于它不会让我时常懵逼。javascript

每次作完项目事后,都会被本身庞大的ViewController代码吓坏,无论是什么网络请求、networking data process、跳转交互逻辑通通往ViewController里面塞,就算是本身写的代码,也不敢直视。我不得不思考是否是MVC模式太过落后了,毕竟它叫作Massive View Controller,其实说MVC落后不太合理,说它太原生了比较合适。java

MVC模式的历史很是的久远,它其实不过是对编程模式的一种模块化,无论是MVVM、MVCS、仍是听起来就不寒而栗的VIPER,都是对MVC标准的三个模块的继续划分,细分下去,使每一个模块的功能更加的独立和单一,而最终目的都是为了提高代码的规范程度,解耦,和下降维护成本。具体用什么模式须要根据项目的需求来决定,而这里,我简单的说说本身对MVVM架构的理解和设计思想,浅谈拙见。程序员

二:MVVM模块划分


传统的MVC模式分为:Model、View、Controller。Model是数据模型,有胖瘦之分,View负责界面展现,而Controller就负责剩下的逻辑和业务,瞬间Controller心中一万个草泥马奔腾而过。编程

MVVM模式只是多了一个ViewModel,它的做用是为Controller减负,将Controller里面的逻辑(主要是弱业务逻辑)转移到自身,其实它涉及到的工做不止是这些,还包括页面展现数据的处理等。(后序章节会有具体讲解)数组

个人设计是这样的:网络

  • 一个View对应一个ViewModel,View界面元素属性与ViewModel处理后的数据属性绑定
  • Model只是在有网络数据的时候须要建立,它的做用只是一个数据的中专站,也就是一个极为简介的瘦model
  • 这里弱化了Model的做用,而将对网络数据的处理的逻辑放在ViewModel中,也就是说,只有在有网络数据展现的View的ViewModel中,才会看见Model的影子,而处理事后的数据,将变成ViewModel的属性,注意一点,这些属性必定要尽可能“直观”,好比能写成UIImage就不要写成URL
  • ViewModel和Model能够视状况看是否须要属性绑定
  • Controller的做用就是将主View经过与之对应的ViewModel初始化,而后添加到self.view,而后就是监听跳转逻辑触发等少部分业务逻辑,固然,ViewController的跳转仍是须要在这里实现。 注意:这里面提到的绑定,其实就是对属性的监听,当属性变化时,监听者作一些逻辑处理,强大的框架来了————RAC

三:ReactiveCocoa

RAC是一个强大的工具,它和MVVM模式的结合使用只能用一个词形容————完美。架构

固然,有些开发者不太愿意用这些东西,大概是由于他们以为这破坏了代理、通知、监听、block等的复杂逻辑观感,可是我在这里大力推崇RAC,由于个人MVVM搭建思路里面会涉及大量的属性绑定、事件传递,我可不想写上一万个协议来实现这些简单的功能,运用RAC能大量简化代码,使逻辑更加的清晰。框架

接下来我将对个人MVVM架构实现思路作一个详细的讲解,在这以前,若是你没有用过RAC,请先移步:ide

大体的了解一下RAC事后,即可以往下(^)模块化

四:MVVM模块具体实现


这是要实现的界面:

AF45BFF3B07B52D222AF90AE1CCBAC18.png

一、Model

这里我弱化了Model的做用,它只是做为一个网络请求数据的中转站,只有在View须要显示网络数据的时候,对应的ViewModel里面才有Model的相关处理。

二、ViewModel

在实际开发当中,一个View对应一个ViewModel,主View对应而且绑定一个主ViewModel。

主ViewModel承担了网络请求、点击事件协议、初始化子ViewModel而且给子ViewModel的属性赋初值;网络请求成功返回数据事后,主ViewModel还须要给子ViewModel的属性赋予新的值。

主ViewModel的观感是这样的:

@interface MineViewModel : NSObject //viewModel @property (nonatomic, strong) MineHeaderViewModel *mineHeaderViewModel; @property (nonatomic, strong) NSArray<MineTopCollectionViewCellViewModel *> *dataSorceOfMineTopCollectionViewCell; @property (nonatomic, strong) NSArray<MineDownCollectionViewCellViewModel *> *dataSorceOfMineDownCollectionViewCell; //RACCommand @property (nonatomic, strong) RACCommand *autoLoginCommand; //RACSubject @property (nonatomic, strong) RACSubject *pushSubject; @end

其中,RACCommand是放网络请求的地方,RACSubject至关于协议,这里用于点击事件的代理,而ViewModel下面的一个ViewModel属性和三个装有ViewModel的数组我须要着重说一下。

在iOS开发中,咱们一般会自定义View,而自定义的View有多是继承自UICollectionviewCell(UITableViewCell、UITableViewHeaderFooterView等),当咱们自定义一个View的时候,这个View不须要复用且只有一个,咱们就在主ViewModel声明一个子ViewModel属性,当咱们自定义一个须要复用的cell、item、headerView等的时候,咱们就在主ViewModel中声明数组属性,用于储存复用的cell、item的ViewModel,中心思想仍然是一个View对应一个ViewModel。

在.m文件中,对这些属性作懒加载处理,而且将RACCommand和RACSubject配置好,方便以后在须要的时候触发以及调用,代码以下:

@implementation MineViewModel

- (instancetype)init { self = [super init]; if (self) { [self initialize]; } return self; } - (void)initialize { [self.autoLoginCommand.executionSignals.switchToLatest subscribeNext:^(id responds) { //处理网络请求数据 ...... }]; } #pragma mark *** getter *** - (RACSubject *)pushSubject { if (!_pushSubject) { _pushSubject = [RACSubject subject]; } return _pushSubject; } - (RACCommand *)autoLoginCommand { if (!_autoLoginCommand) { _autoLoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { NSDictionary *paramDic = @{......}; [Network start:paramDic success:^(id datas) { [subscriber sendNext:datas]; [subscriber sendCompleted]; } failure:^(NSString *errorMsg) { [subscriber sendNext:errorMsg]; [subscriber sendCompleted]; }]; return nil; }]; }]; } return _autoLoginCommand; } - (MineHeaderViewModel *)mineHeaderViewModel { if (!_mineHeaderViewModel) { _mineHeaderViewModel = [MineHeaderViewModel new]; _mineHeaderViewModel.headerBackgroundImage = [UIImage imageNamed:@"BG"]; _mineHeaderViewModel.headerImageUrlStr = nil; [[[RACObserve([LoginBackInfoModel shareLoginBackInfoModel], headimg) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) { if (x == nil) { _mineHeaderViewModel.headerImageUrlStr = nil; } else { _mineHeaderViewModel.headerImageUrlStr = x; } }]; ...... return _mineHeaderViewModel; } - (NSArray<MineTopCollectionViewCellViewModel *> *)dataSorceOfMineTopCollectionViewCell { if (!_dataSorceOfMineTopCollectionViewCell) { MineTopCollectionViewCellViewModel *model1 = [MineTopCollectionViewCellViewModel new]; MineTopCollectionViewCellViewModel *model2 = [MineTopCollectionViewCellViewModel new]; ...... _dataSorceOfMineTopCollectionViewCell = @[model1, model2]; } return _dataSorceOfMineTopCollectionViewCell; } - (NSArray<MineDownCollectionViewCellViewModel *> *)dataSorceOfMineDownCollectionViewCell { if (!_dataSorceOfMineDownCollectionViewCell) { ...... } return _dataSorceOfMineDownCollectionViewCell; } @end

为了方便,我直接将之前写的一些代码贴上来了,不要被它的长度吓着了,你彻底能够忽略内部实现,只须要知道,这里不过是实现了RACCommand和RACSubject以及初始化子ViewModel。

是的,主ViewModel的主要工做基本上只有这三个。

关于属性绑定的逻辑,我将在以后讲到。

咱们先来看看子ViweModel的观感:

@interface MineTopCollectionViewCellViewModel : NSObject @property (nonatomic, strong) UIImage *headerImage; @property (nonatomic, copy) NSString *headerTitle; @property (nonatomic, copy) NSString *content; @end

我没有贴.m里面的代码,由于里面没有代码(嘿嘿)。

接下来讲说,为何我设计的子ViewModel只有几个单一的属性,而主ViewModel却有如此多的逻辑。

首先,咱们来看一看ViewModel的概念,Model是模型,因此ViewModel就是视图的模型。而在传统的MVC中,瘦Model叫作数据模型,其实瘦Model叫作DataModel更为合适;而胖Model只是将网络请求的逻辑、网络数据处理的逻辑写在了里面,方便于View更加便捷的展现数据,因此,胖Model的功能和ViewModel大同小异,我把它叫作“少根筋的ViewModel”。

这么一想,咱们彷佛应该将网络数据处理的逻辑放在子ViewModel中,来为主ViewModel减负。

我也想这么作。

可是有个问题,举个简单的例子,好比这个需求:

通常的思路是自定义一个CollectionviewCell和一个ViewModel,由于它们的布局是同样的,咱们须要在主ViewModel中声明一个数组属性,而后放入两个ViewModel,分别对应两个Cell。

image和title这种静态数据咱们能够在主ViewModel中为这两个子ViewModel赋值,而下方的具体额度和数量来自网络,网络请求下来的数据一般是:

{
    balance:"100" redPacket:"3" }

咱们须要把”100“转化为”100元“,”3“转化为”3个“。 这个网络数据处理逻辑按正常的逻辑来讲是应该放在ViewModel中的,可是有个问题,咱们这个collectionviewcell是复用的,它的ViewModel也是同一个,而处理的数据是两个不一样的字段,咱们如何区分?并且不要忘了,网络请求成功得到的数据是在主ViewModel中的,还涉及到传值。再按照这个思路去实现必然更为复杂,因此我干脆一刀切,无论是静态数据仍是网络数据的处理,统统放在主ViewModel中。

这样作虽然让主ViewModel任务繁重,子ViewModel过于轻量,可是带来的好处却不少,一一列举:

  • 在主ViewModel的懒加载中,实现对子ViewModel的初始化和赋予初值,在RACCommand中网络请求成功事后,主ViewModel须要再次给子ViewModel赋值。赋值条理清晰,两个模块。
  • 子ViewModel只放其对应的View须要的数据属性,做用至关于Model,可是比Model更加灵活,由于若是该View内部有着一些点击事件等,咱们一样能够在子ViewModel中添加RACSubject(或者协议)等,子ViewModel的灵活性很高。
  • 无论是静态数据仍是网络数据统一处理,全部子ViewModel的初始化和属性赋值放在一起,全部网络请求放在一起,全部RACSubject放在一起,结构更加清晰,维护方便。

三、View

以前讲到,ViewModel和Model交互的惟一场景是有网络请求数据须要展现的状况,而View和ViewModel倒是一一对应,绑不绑定须要视状况而定。下面详细介绍。

自定义View这里分两种状况,分别处理:

(1)非继承有复用机制的View(不是继承UICollectionviewCell等)

这里以界面的主View为例

.h

- (instancetype)initWithViewModel:(MineViewModel *)viewModel;

该View须要和ViewModel绑定,实现相应的逻辑和触发事件,而且保证ViewModel的惟一性。

.m

这里就不贴代码了,反正View与ViewModel的交互无非就是触发网络请求、触发点击事件、将ViewModel的数据属性展现在界面上。若是你会一些RAC,固然实现这些就是小菜一碟,可是若是你坚持苹果原生的协议、通知,实现起来就会有一点麻烦(代码量啊!!!)。

(2)继承有复用机制的View(UICollectionviewCell等)

最值得注意的地方就是cell、item的复用机制问题了。

咱们在自定义这些cell、item的时候,并不能绑定相应的ViewModel,由于它的复用原理,将会出现多个cell(item)的ViewModel如出一辙,在这里,我选择了一个我自认为最好的方案来解决。

首先,在自定义的cell(item).h中声明一个ViewModel属性。

#import <UIKit/UIKit.h> #import "MineTopCollectionViewCellViewModel.h" @interface MineTopCollectionViewCell : UICollectionViewCell @property (nonatomic, strong) MineTopCollectionViewCellViewModel *viewModel; @end

而后,在该属性的setter方法中给该cell的界面元素赋值:

#pragma mark *** setter *** - (void)setViewModel:(MineTopCollectionViewCellViewModel *)viewModel { if (!viewModel) { return; } _viewModel = viewModel; RAC(self, contentLabel.text) = [[RACObserve(viewModel, content) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal]; self.headerImageView.image = viewModel.headerImage; self.headerLabel.text = viewModel.headerTitle; }

ps:这里再次看到RAC()和RACObserve()这两个宏,这是属性绑定,若是你不懂,能够先不用管,在后面我会讲解一下个人属性绑定思路,包括不使用ReactiveCocoa达到一样的效果(这彻底是做死啊!!!)。

重写setter的做用你们应该知道吧,就是在collection view的协议方法中写到:

cell.viewModel = self.viewModel.collectionCellViewModel;

的时候,可以执行到该setter方法中,改变该cell的布局。

好吧,这就是精髓,废话不说了。

想了一下,仍是贴上主View的.m代码吧(再次强调,重在思想):

@interface MineView () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout> @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) MineViewModel *viewModel; @end @implementation MineView - (instancetype)initWithViewModel:(MineViewModel *)viewModel { self = [super init]; if (self) { self.backgroundColor = [UIColor colorWithRed:243/255.0 green:244/255.0 blue:245/255.0 alpha:1]; self.viewModel = viewModel; [self addSubview:self.collectionView]; [self setNeedsUpdateConstraints]; [self updateConstraintsIfNeeded]; [self bindViewModel]; } return self; } - (void)updateConstraints { [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.mas_equalTo(self); }]; [super updateConstraints]; } - (void)bindViewModel { [self.viewModel.autoLoginCommand execute:nil]; } #pragma mark *** UICollectionViewDataSource *** - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return 3; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if (section == 1) return self.viewModel.dataSorceOfMineTopCollectionViewCell.count; ...... } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 1) { MineTopCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineTopCollectionViewCell class])] forIndexPath:indexPath]; cell.viewModel = self.viewModel.dataSorceOfMineTopCollectionViewCell[indexPath.row]; return cell; } ...... } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { ...... } #pragma mark *** UICollectionViewDelegate *** - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { [self.viewModel.pushSubject sendNext:nil]; } #pragma mark *** UICollectionViewDelegateFlowLayout *** ...... #pragma mark *** Getter *** - (UICollectionView *)collectionView { if (!_collectionView) { ...... } return _collectionView; } - (MineViewModel *)viewModel { if (!_viewModel) { _viewModel = [[MineViewModel alloc] init]; } return _viewModel; } @end

四、Controller

这家伙已经解放了。

@interface MineViewController () @property (nonatomic, strong) MineView *mineView; @property (nonatomic, strong) MineViewModel *mineViewModel; @end @implementation MineViewController #pragma mark *** life cycle *** - (void)viewDidLoad { [super viewDidLoad]; self.hidesBottomBarWhenPushed = YES; [self.view addSubview:self.mineView]; [AutoLoginAPIManager new]; [self bindViewModel]; } - (void)updateViewConstraints { [self.mineView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.mas_equalTo(self.view); }]; [super updateViewConstraints]; } - (void)bindViewModel { @weakify(self); [[self.mineViewModel.pushSubject takeUntil:self.rac_willDeallocSignal] subscribeNext:^(NSString *x) { @strongify(self); [self.navigationController pushViewController:[LoginViewController new] animated:YES]; }]; } #pragma mark *** getter *** - (MineView *)mineView { if (!_mineView) { _mineView = [[MineView alloc] initWithViewModel:self.mineViewModel]; } return _mineView; } - (MineViewModel *)mineViewModel { if (!_mineViewModel) { _mineViewModel = [[MineViewModel alloc] init]; } return _mineViewModel; } @end

是否是很是清爽,清爽得甚至怀疑它的存在感了(_)。

五:附加讲述


一、绑定思想

我想,懂一些RAC的人都知道属性绑定吧,RAC(,)和RACObserve(,),这是最经常使用的,它的做用是将A类的a属性绑定到B类的b属性上,当A类的a属性发生变化时,B类的b属性会自动作出相应的处理变化。

这样就能够解决至关多的需求了,好比:用户信息展现界面->登陆界面->登陆成功->回到用户信息展现界面->展现用户信息

以往咱们的作法一般是,用户信息展现界面写一个通知监听->登陆成功发送通知->用户信息展现界面刷新布局

固然,也能够用协议、block什么的,这么一看貌似并无多么复杂,可是一旦代码量多了事后,你就知道什么叫懵逼了,而使用RAC的属性绑定、属性联合等一系列方法,将会有事半功倍的效果,充分的下降了代码的耦合度,下降维护成本,思路更清晰。

在上面这个需求中,须要这样作:

将用户信息展现View的属性,好比self.name,self.phone等与对应的ViewModel中的数据绑定。在主ViewModel中,为该子ViewModel初始化并赋值,用户信息展现View的内容就是这个初始值。当主ViewModel网络请求成功事后,再一次给该子ViewModel赋值,用户信息展现界面就能展现相应的数据了。

是否是很叼,你什么都不用作,毫无污染。

并且,咱们还能够作得更好,就像我以上的代码里面作的(可能有点乱,很差意思),将View的展现内容与ViewModel的属性绑定,将ViewModel的属性与Model的属性绑定,看个图吧:

这里写图片描述

只要Model属性一变,传递到View使界面元素变化,全自动无添加。有了这个东西事后,之后reloadData这个方法可能见得就比较少了。

二、总体逻辑梳理

  1. 进入ViewController,懒加载初始化主View(调用-initWithViewMdoel方法,保证主ViewModel惟一性),懒加载初始化主ViewModel。
  2. 进入主ViewModel,初始化配置网络请求、点击逻辑、初始化各个子ViewModel。
  3. 进入主View,经过主ViewModel初始化,调用ViewModel中的对应逻辑和对应子ViewModel展现数据。
  4. ViewController与ViewModel的交互主要是跳转逻辑等。

三、建立本身的架构

其实在任何项目中,若是某一个模块代码量太大,咱们彻底能够本身进行代码分离,只要遵循必定的规则(固然这是本身定义的规则),最终的目的都是让功能和业务细化,分类。

这至关于在沙滩上抓一把沙,最开始咱们将石头和沙子分开,可是后来,发现沙子也有大有小,因而咱们又按照沙子的大小分红两部分,再后来发现沙子颜色太多,咱们又把不一样颜色的沙子分开……

在MVVM模式中,彻底能够把ViewModel的网络请求逻辑提出来,叫作NetworkingCenter;还能够把ViewModel中的点击等各类监听事件提出来,叫作ActionCenter;还能够把界面展现的View的各类配置(好比在tableView协议方法中的写的数据)提出来,叫作UserInterfaceConfigDataCenter;若是项目中须要处理的网络请求数据不少,咱们能够将数据处理逻辑提出来,叫作DataPrecessCenter ……

记住一句话:万变不离其宗。

六:结语

移动端的架构一直都是变幻无穷,没有万能的架构,只有万能的程序员,根据产品的需求选择相应的架构才是正确的作法,MVC当然古老,可是在小型项目却依然实用;MVVM+RAC虽然很强大,可是在有时候仍是会增长代码量,其实MVVM和Android里面的MVP模式有至关多的共同点,能够借鉴了解;至于MVCS没有什么可讲的,VIPER模式看起来比较厉害,想想可能又是把哪一个模块细化了,猜想ViewModel?嘿嘿,其实我没研究过VIPER,就不班门弄斧了。

相关文章
相关标签/搜索