通俗易懂图解MVVM和RAC双向绑定介绍(附Demo)
Posted Deft_MKJing宓珂璟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通俗易懂图解MVVM和RAC双向绑定介绍(附Demo)相关的知识,希望对你有一定的参考价值。
前言
一个前辈的MVVM介绍
其实MVVM就是MVC的进化版本,相对于臃肿的Controller,代码越来越多之后,有一部分人就用了新的设计模式,其实看久了也没什么,通俗点讲,其实就是把之前Controller里面的代码逻辑全部移植到了ViewModel里面,相对于以前而言,控制器也被归属于View一类,那么他和View一样都会有自己的ViewModel去处理逻辑,而且ViewModel必然拥有Model,这样的关系使得控制器代码会减少很多很多,处理起来又多了一个类,本身设计模式里面有代理,通知,KVO等,不同业务对应不同的设计模式,个人理解为了减少控制器的代码,引进了新的类,那么类的交互就变得更麻烦了,因此RAC出现了,他帮我们直接管理了苹果的那一套数据处理设计模式,统一用它的”信号流”来进行,谁用谁知道啊。。。。。。
双向绑定
1.Model—->View 这种流向很简单,你请求数据之后,通过Block的回调,最终更新UI
2.View—–>Model 反向绑定也一样,View触发事件,更新对面ViewModel里面绑定的数据源,例如登录注册的Textfield,你输入和删除的时候,你的Model字段会对应更新,当你提交的时候,读取ViewModel的字段,就是已经更新的最新数据。这是一种方式,我个人感觉如下图的另一种更容易理解,比如你选择某个cell或者点赞的时候,View事件触发,更新绑定的ViewModel字段,拥有ViewModel的控制器,用RACObserve来进行该字段开关的读取,如果监听到YES,就刷新对应的页面UI
简单看下自己理解的MVVM
效果图
图片和文字闪烁的效果传送门
闪烁效果
RAC的第一种流程介绍—>RACSignal
网上很多基本的介绍,这里主要讲一下流程
1.如果用RACSignal
来创建信号(内部Block有发送信号以及取消信号的回调,为什么是3和4呢,原因在后面)
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
// 3.发送信号
[subscriber sendNext:@"mkj"];
[subscriber sendCompleted];
// 4.取消信号,如果信号想要被取消,就必须返回一个RACDisposable
// 当信号被手动或者自动取消订阅之后会回调到这里,进行一些资源的释放
return [RACDisposable disposableWithBlock:^
NSLog(@"取消订阅");
];
];
注意:上面创建的方法内部代码主要归结于创建一个集成于RACSignal
的子类—>RACDynamicSignal
,然后
通过静态方法实例化出来,并把传进去的任务Block进行对象属性的存储
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
RACDynamicSignal *signal = [[self alloc] init];
signal->_didSubscribe = [didSubscribe copy];
return [signal setNameWithFormat:@"+createSignal:"];
2.创建的信号RACSignal(子类RACDynamicSignal)来调用第二步[Signale subscribeNext:^];
来进行信号的订阅,内部转换代码
RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
return [self subscribe:o];
注意:这里RAC的设计者向我们隐藏了RACSubscriber
,对外暴露了RACSignal
,所有的内部工作都由RACSubsriber
进行完成传递
如果你再点进去,这里传递的Signal,会判断之前创建的时候传进的Block是否为空,如果有任务,直接回调Block
RACDisposable *innerDisposable = self.didSubscribe(subscriber);
3.现在有人订阅了,又回调了Block,然后触发任务,完成之后调用
[subscriber sendNext:@"mkj"];
[subscriber sendCompleted];
// 内部代码最终调用方法如下
- (void)sendNext:(id)value
@synchronized (self)
void (^nextBlock)(id) = [self.next copy];
if (nextBlock == nil) return;
nextBlock(value);
最终回调到订阅的时候NextBlock的任务
4.这个可有可无,返回一个RACDisposable,对订阅取消进行资源的释放
总结:把他比喻成工厂,当你需要打开生产流水线的时候(创建信号,带有任务),这个时候你工人都没有,根本不会走你的任务
信号不会被传递,而你有工人来的时候(就是订阅了信号),这个时候流水线才开始进行加工,这就是个人理解的冷信号模式
也可以把冷信号理解为未被订阅的信号,理解为信号里面的任务是不会进行的,只有订阅者的加入,信号才会变成热信号
也就是这玩意需要手动开启信号
RAC第二种的流程介绍—>RACSubject(继承与RACSignal)
1.创建信号
RACSubject *subject = [RACSubject subject];
该方法和上面的创建方式有所不同,他的实例化出来之后只是创建了一个数组,专门用来存储订阅信号的订阅者
2.订阅信号
[subject subscribeNext:^(id x)
// 当信号sendNext的时候会回调
NSLog(@"%@",x);
];
// 这方法也是和上面的有所区别,RACSubject该对象会把订阅者放到之前创建的数组里面,然后啥都不做了
3.[subject sendNext:value];
内部代码
[self enumerateSubscribersUsingBlock:^(id<RACSubscriber> subscriber)
[subscriber sendNext:value];
];
可以看出,当他调用sendNext的时候,是会进行数组的遍历,然后挨个对订阅者发送消息
总结:其实这就是所谓的热信号模式,还是拿工厂来做比喻,RACSubject
是不管你有没有人订阅,我工厂24小时开启流水线
我管你有没有人加工,有人来了,我就用数组登记一下,信号来了的时候你们就负责接收任务,没人的时候我还是就好比我的员工
花名册是空的,但是照样生产,只是没人做事罢了,那么这里的RAC信号流就是没人处理罢了,会被丢弃
知识点:区别RACSubject和RACSignal
1.我个人理解,前者是冷信号模式,需要有人订阅才开启热信号,后者是热信号默认,不管你有没有订阅
2.前者其实是一旦有人订阅,就会去做对应的一组信号任务,然后进行回调,可以理解为有人的时候任务启动,没人的时候挂机
没错,我是把它简单理解为代理,后者是热信号,信号负责收集订阅者数组,发信号的时候回遍历订阅者,一个个执行任务
你可以把它理解为通知,我管你有没有接收,我照样发送,没人就丢弃
3.前者个人用来进行网络请求,后者进行类似代理或者通知的数据传递模式,这样就可以简单的理解为,RAC其实就是把apple的一套
delegate,Notification,KVO等一系列方法综合起来了,用起来更舒服罢了
4.那么MVVM模式下,本身就多了个ViewModel,交互起来需要更多的设计模式协助,RAC就解决了这个问题,直接用这个设计模式来搞
数据传递和监听的代码就清晰很多了
既然已经了解了RAC的流程,Demo走起!!!
MVVM + RAC示例Demo
1.用到了网络请求的信号传递
2.用RACObserve宏进行属性的KVO观察
3.用RACSubject进行数据的回调
4.用RACSequence进行异步数组和字典(打印的是RACTuple)的遍历
5.RAC–>combineLatest的方法进行简单多输入框登录注册页面模拟
先看下Demo里面各个类的关系
1.首先看下用MVVM的基类(MKJBaseViewController)
// baseVC的基础ViewModel
// 子类重写就能覆盖类型
@property (nonatomic,strong,readonly) MKJBaseViewModel *viewModel;
/**
唯一初始化方法
@param viewModel 传入ViewModel
@return 实例化控制器对象
*/
- (instancetype)initWithViewModel:(MKJBaseViewModel *)viewModel;
/**
布局UI 子类重写
*/
- (void)setupLayout;
/**
请求网络数据 绑定数据 子类重写
*/
- (void)setupBinding;
/**
设置数据回调,点击事件处理 子类重写
*/
- (void)setupData;
初始化的时候
MKJDemoViewModel *viewModel = [[MKJDemoViewModel alloc] init];
MKJDemoViewController *demoVC = [[MKJDemoViewController alloc] initWithViewModel:viewModel];
[self.navigationController pushViewController:demoVC animated:YES];
2.再看一下TableViewController的基类
基础的实现和普通拥有tableView的控制器一样,无非区别在于代理的逻辑和数据交给了ViewModel
// 交给子类实现,传递最终的cell类
- (Class)cellClassForRowAtIndexPath:(NSIndexPath *)indexPath
@throw [NSException exceptionWithName:@"抽象方法未实现"
reason:[NSString stringWithFormat:@"%@ 必须实现抽象方法 %@",[self class],NSStringFromSelector(_cmd)]
userInfo:nil];
#pragma mark - tableView datasource
// 交给ViewModel去实现
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
return [self.viewModel numberOfSections];
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
return [self.viewModel numberOfRowInSection:section];
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
return [self.viewModel heightForHeaderInSection:section];
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
return [self.viewModel viewForHeaderInSection:section];
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
MKJBaseTableViewCell *cell = [[self cellClassForRowAtIndexPath:indexPath] cellForTableView:tableView viewModel:[self.viewModel cellViewModelForRowAtIndexPath:indexPath]];
cell.selectionStyle = [self.viewModel tableViewCellSelectionStyle];
return cell;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
CGFloat height = tableView.rowHeight;
NSNumber *calculateHeight = [[self cellClassForRowAtIndexPath:indexPath] calculateRowHeightWithViewModel:[self.viewModel cellViewModelForRowAtIndexPath:indexPath]];
if (calculateHeight)
height = calculateHeight.floatValue;
return height;
3.最终上层的ViewController核心代码,最终剩下就这么点代码了,瘦身不。。。。
// 基本布局代码,顺便设置个RACObserve
- (void)setupLayout
[super setupLayout];
@weakify(self);
[RACObserve(self.viewModel, isNeedRefresh) subscribeNext:^(id x)
@strongify(self);
if ([x boolValue])
[self.tableView reloadData];
];
// ViewModel进行网络请求
- (void)setupBinding
[super setupBinding];
@weakify(self)
[self.viewModel sendRequest:^(id entity)
@strongify(self);
[self hideLoadingViewFooter];
[self.tableView reloadData];
failure:^(NSUInteger errCode, NSString *errorMsg)
];
// 返回对应的CellClass
- (Class)cellClassForRowAtIndexPath:(NSIndexPath *)indexPath
return [MKJDemoTableViewCell class];
4.来看下ViewModel在做什么,核心还是网络请求,RAC信号流的网络请求
// 网络请求外部
- (void)sendRequest:(MKJRequestSucceed)succeedBlock failure:(MKJRequestFailure)failBlock
[[self.model requestDemoDatasWithPage:[self.currentPage integerValue] maxTime:self.currentMaxTime] subscribeNext:^(id data)
if (data)
self.entity = data;
[self handlePagingEntities:self.entity.list
totalCount:@(self.entity.info.count)
cellViewModelClass:[MKJDemoTableViewCellViewModel class]
maxTime:self.entity.info.maxtime];
!succeedBlock ? : succeedBlock(data);
];
// 网络请求内部
- (RACSignal *)getRequestWithURLString:(NSString *)URLString
parametersDictionary:(NSDictionary *)paraterDictionary
parserEntityClass:(Class)parseEntityClass
// 根据异步请求创建一个新的RACSinal
@weakify(self)
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
@strongify(self);
[self.httpHelper getRequestWithUrlString:URLString
parametersDictonary:paraterDictionary
entityClass:parseEntityClass
completeBlock:^(id data)
[subscriber sendNext:data];
[subscriber sendCompleted];
];
return nil;
];
根据之前上面介绍的RAC逻辑,外部注册订阅者的时候成为热信号,然后调用创建信
号的Block,完成网络请求之后sendNext进行回调,设计如此,然后在ViewModel中
把Model组装完毕,进行外部的TableView reload。然后再次调用代理方法的时候,
会再次进到ViewModel里面获取已经组装好的数据返回给TableView的DataSource
,OK了
5.Cell也单独配置了对应CellViewModel,就是在RAC网络请求回来之后,把实体Model,用CellViewModel来进行组装,只是把之前装数据Model的数组,用来装拥有数据模型的CellViewModel,明白一点,ViewModel拥有Model,就能搞定数据的逻辑处理
我TM的终于明白了,NO BB Show me the code了,这根本说不清楚,需要的同学还是直接看Demo吧,最终的逻辑转换就是上面的MVVM效果图,理解了就可以了,无非就是一种设计思想,不过加上RAC确实还不错。。。
我个人也比较喜欢看Demo,需要的还是直接开撸吧
正确Demo地址
以上是关于通俗易懂图解MVVM和RAC双向绑定介绍(附Demo)的主要内容,如果未能解决你的问题,请参考以下文章