RAC+MVVM的项目实例演练

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有别的理解的,很高兴你能为我指正,感激不尽,最后再附上源代码地址

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

推荐阅读更多精彩内容