ps:最近学习了ReactiveCocoa(RAC),就用这个结合MVVM的思想弄了个小项目,项目源码已经上传到GitHub上,有兴趣的同胞可以下载下来,源码地址,下面我就抽出一个界面来介绍一下如何使用RAC+MVVM,例子是经典的tableView类型。
先附上一张结构图
屏幕快照 2017-10-12 16.32.35.png从图中可以清晰地看到项目结构:MVVM 各自对应的位置,具体的MVVM原理,网上资料一大堆,这里就不做赘述,我简要说一下,各自的部分都做了哪些功能:
M:这个不用说,是model层,我这里处理比较简单,只是单纯的用来处理数据转模型
V:view,主要用于数据展现
VM:这里是MVVM出现的重点所在,它主要用来处理数据分析和一些业务逻辑处理,我这里是将网络请求以及告诉view层展现数据的业务都放在了这里(这里我在做的时候也有疑问,告诉view的动作究竟是由controller做好,还是放到vm中好,最后看了一些资料,觉得放在vm中更加理想,前提是在控制器中就建立好vm和view的联系,也就是将view绑定到vm中),下面的例子可以看到。
最后就说一下Controller了,在MVC中Controller是用来沟通M和V的,它既要知道M何时发生了改变,又要随时准备告诉V去改变视图展现,相应的一些业务逻辑处理也只能丢到C中,导致了C的臃肿,在MVVM中,可以极大地去减轻控制器的负担,从某种程度上,比如网络请求,数据分析以及一些业务逻辑都可以放到VM中。C这个时候也需要充当中间人的角色,只不过它不用再去监控M层变化,也不需要告诉View层改变数据展示,这些都可以由VM来代劳。我看过一篇文章,说的是控制器只需要处理必须放到控制器的逻辑,例如页面跳转,view的初始化,VM的初始化等,我深以为然,在这个例子中我也是这样处理的。
下面,先开始从控制器层说起:
1. 控制器: WLHomeController
实现功能
: 首页的内容是类似于新闻首页,既有顶部标签(我这里处理的较简单,顶部没有滚动选择功能),内容视图又可以左右滚动查看,同时上下可以联动,又能保证再次回到出现过得界面不会再次自动发送网络请求。
-(WLTopTagView *)topTagView{
if (!_topTagView){
_topTagView = [[WLTopTagView alloc] initWithFrame:CGRectMake(0, kNavigationBarH, self.view.frame.size.width, 30)];
[self.view addSubview:_topTagView];
}
return _topTagView;
}
-(UIScrollView *)mainScrollView{
if (!_mainScrollView){
_mainScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, CGRectGetMaxY(self.topTagView.frame) + 1,kScreenW, kScreenH -CGRectGetMaxY(self.topTagView.frame) - 1)];
_mainScrollView.backgroundColor = [UIColor whiteColor];
_mainScrollView.pagingEnabled = YES;
_mainScrollView.delegate = self;
[self.view addSubview:_mainScrollView];
}
return _mainScrollView;
}
- (NSMutableArray *)listTableViewArray {
if (!_listTableViewArray) {
_listTableViewArray = [NSMutableArray array];
}
return _listTableViewArray;
}
-(WLHomtTopViewModel *)topViewModel{
if (!_topViewModel){
_topViewModel = [[WLHomtTopViewModel alloc] init];
}
return _topViewModel;
}
-(NSMutableArray *)viewModelArray{
if (!_viewModelArray){
_viewModelArray = [NSMutableArray array];
}
return _viewModelArray;
}
这里是懒加载初始化必要视图及数据.
设计方案
: 我采用的是UIScrollView
+ UITableView
的结构,有兴趣的同胞可以尝试一下UICollectionView
+ UITableView
的结构来实现
我这里没有采用复用几个tableView的思想,这里是可以优化的点,可以复用两个或者三个tableView去节约内存。
将view和viewModel绑定
:
[viewModel bindViewToViewModel:tableView];
主要是通过viewModel中提供的接口
- (void)bindViewToViewModel:(UIView *)view {
self.tableView = (UITableView *)view;
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self.tableView registerNib:[UINib nibWithNibName:@"WLHomeListCell" bundle:nil] forCellReuseIdentifier:@"listCell"];
}
题外话
:
- (void)bindViewToViewModel:(UIView *)view
再好一点的做法是创建一个VM基类,把这个方法抽出来,所有的VM都继承于这个基类,分别取实现这个方法。我比较懒,一开始没考虑到这种情况,后来就不想改了,凑合着看吧
到这里你可能会有疑问,那么如何去使用RAC呢?下面我就来简要说一下如何去用,如何建立起控制器和VM之间的关系:
2.如何运用RAC+VM
@property (nonatomic,strong,readonly) RACCommand *homeListCommand;
连接VM和C的东西就是它 ,关于RAC的原理和实现我也讲不出来,只会用,如果你想了解,可以去搜索一些RAC的资料,自行学习撒~~~
在VM中需要这样做:
//获取首页列表数据
- (void)requestHomeListInfo {
@weakify(self);
_homeListCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
if (!self.firstLoadData) {
return [RACSignal empty];
}
self.firstLoadData = NO;
RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[WLNetworkTool sharedInstance] loadHomeListData:[input intValue] success:^(id response) {
[subscriber sendNext:response];
[subscriber sendCompleted];
} failure:^(NSError *error) {
[subscriber sendError:error];
[subscriber sendCompleted];
}];
return nil;
}];
return [requestSignal map:^id(NSDictionary *value) {
NSArray *data = value[@"data"][@"items"];
NSArray *modelsArray = [[data.rac_sequence map:^id(id value) {
return [WLHomeListModel mj_objectWithKeyValues:value];
}] array];
//这一步刷新列表可以放到这里,也可以放到控制器里面
NSLog(@"请求首页列表数据成功 %@",modelsArray);
if (modelsArray && modelsArray.count > 0) {
[self.homeListArray removeAllObjects];
[self.homeListArray addObjectsFromArray:modelsArray];
[self.tableView reloadData];
}
return modelsArray;
}];
}];
}
RACCommand内部是拥有一个signal的,我们所谓的网络请求也就是在这个里面去弄,至于何时触发,下面再介绍,现在先来把这一段给简要说一下:
内部信号requestSignal
里面实现的东西需要用[requestSignal map:^id(NSDictionary *value)
触发,你打断点可以看到,先执行的是[requestSignal map:^id(NSDictionary *value)
,而后信号激活变成热信号,才会去执行信号里面的内容,也就是网络请求。
网络请求成功后,会执行map里面的内容,我这里为了熟悉RAC,特意用了map去对数据进行转换,想省事,可以在这里直接用MJExtension就行,最后才会走到控制器中订阅的block之中
[requestSignal subscribeNext:^(NSArray *x) {
// self.currentTableView = self.listTableViewArray[index];
NSLog(@"请求首页列表数据成功 %@",x);
// if (x && x.count > 0) {
// [viewModel.homeListArray removeAllObjects];
// [viewModel.homeListArray addObjectsFromArray:x];
// [self.currentTableView reloadData];
// }
}];
那么这个command又是什么时候执行的呢,这就需要控制器去触发执行时间了,其实很简单,就一句话
[viewModel.homeListCommand execute:@(model.ID)];
这就是整个触发流程,简单说可以理解为以下几个步骤:
1.在vm中定义command
2.在控制器中将view和vm绑定
3.在控制器中触发command执行时机
4.在vm中进行网络请求并处理数据,通知view刷新数据
这个是控制器和vm之间的通信,通过信号机制解决,那么vm和v之间是如何实现使用RAC的呢?这就需要RAC中的订阅者,顾名思义,订阅之就是先订阅对象,然后在合适的时机触发执行条件,那么订阅的内容就会执行,这个比较类似于OC中的block,其实就是block,先保存要执行的block块,然后在某个时间点触发执行block操作,订阅者的实现也是类似。
3.VM和V之间通信
首先你需要在V中有一个订阅者 @property (nonatomic,strong) RACSubject *cellSubject;
这个订阅者是被V拥有的,触发时机是由外界触发,所以,在V中需要这样写:
self.cellSubject = [RACSubject subject];
@weakify(self);
[self.cellSubject subscribeNext:^(WLHomeListModel *model) {
@strongify(self);
// NSLog(@"传送过来模型了");
self.descLabel.text = model.title;
self.likeControl.text = [NSString stringWithFormat:@"%d",model.likes_count];
self.likeControl.image = model.liked ? [UIImage imageNamed:@"content-details_like_selected_16x16_"] : [UIImage imageNamed:@"Feed_FavoriteIcon_17x17_"];
[self.coverImageView sd_setImageWithURL:[NSURL URLWithString:model.cover_image_url] placeholderImage:[UIImage imageNamed:@"PlaceHolderImage_small_31x26_"]];
self.model = model;
}];
而在VM中需要这样触发:
WLHomeListCell *cell = [tableView dequeueReusableCellWithIdentifier:@"listCell" forIndexPath:indexPath];
[cell.cellSubject sendNext:self.homeListArray[indexPath.row]];
这是不是很像block的使用逻辑呢?当然原理是不一样的,为了方便理解,可以这样认为~~~
那么,有的需求是v中点击某个按钮,需要告诉控制器或者别的类去做相应的操作,这个时候怎么办呢?也需要订阅者参与:
不过不同的是,订阅者的初始化操作不在V中,而是在需要被通知的那个类中,这个例子中就是VM,V是负责激活订阅者的,那么我可不可以这样理解:谁是需要被告知执行某个任务的对象,它就要创建订阅者,谁需要激活订阅者,谁就要执行send操作?这只是我个人的理解,有不对的地方可以指出,能让我更加理解RAC+MVVM的操作>-<
V中有一个喜欢按钮,点击喜欢,需要作出相应的处理:
在V中:
@property (nonatomic,strong) RACSubject *likeSubject;
点击喜欢按钮后,激活订阅者:
[[self.likeControl rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
[self.likeSubject sendNext:@(self.model.ID)];
[self.likeSubject sendCompleted];
}];
VM中创建订阅者,并保存订阅者需要执行的操作:
[cell.likeSubject subscribeNext:^(id x) {
NSLog(@"点击了喜欢 %d",[x intValue]);
}];
这样也就完成了VM和V之间的通讯(正向反向都有)
这只是我个人看了一些RAC资料后练习的小项目,里面肯定有很多问题,如果你有疑问或者对RAC+MVVM有别的理解的,很高兴你能为我指正,感激不尽,最后再附上源代码地址
网友评论