RAC+MVVM的项目实例演练

作者: 皮乐皮儿 | 来源:发表于2017-10-12 17:22 被阅读516次

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

    相关文章

      网友评论

      • 强子ly:demo对入门级选手很系统,good
      • Mister_H:你好,项目运行报错,好多文件都没传上去
        Mister_H:@顾语流年 OK,感谢!
        皮乐皮儿:@Mister_H 另外,您需要重新pod install一下,我上传的时候pod文件是忽略掉的,不然文件太大
        皮乐皮儿:您好,我试了下,从github上下载后需要修改pch地址的,你找一下pch文件地址替换一下,我这边尝试运行正常
      • 焚雪残阳:膜拜大神
        皮乐皮儿:@焚雪残阳 :joy: 你是找抽我看
      • Mr卿:尝试过,没有理解透彻,后来为了项目进度没弄,现在都忘了!再看看。哈哈
      • binya:很有用,谢谢作者
        皮乐皮儿:@玉小火 谢谢阅读,有想法的话随时交流,我也是刚刚尝试这个:smile:

      本文标题:RAC+MVVM的项目实例演练

      本文链接:https://www.haomeiwen.com/subject/kodvextx.html