关闭

iOS架构:MVVM+RAC

标签: iosmvvmRAC
318人阅读 评论(2) 收藏 举报

一:为什么要用MVVM?


为什么要用MVVM?只是因为它不会让我时常懵逼。

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

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,请先移步:

大致的了解一下RAC过后,便可以往下(~^~)

四:MVVM模块具体实现


1、Model

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

2、ViewModel

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

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

主ViewModel的观感是这样的:

#import <Foundation/Foundation.h>
#import "MineHeaderViewModel.h"
#import "MineTopCollectionViewCellViewModel.h"
#import "MineDownCollectionViewCellViewModel.h"

@interface MineViewModel : NSObject

//viewModel
@property (nonatomic, strong) MineHeaderViewModel *mineHeaderViewModel;
@property (nonatomic, strong) NSArray<MineTopCollectionViewCellViewModel *> *dataSorceOfMineTopCollectionViewCell;
@property (nonatomic, strong) NSArray<MineDownCollectionViewCellViewModel *> *dataSorceOfMineDownCollectionViewCell;
@property (nonatomic, strong) NSArray<MineDownCollectionViewCellViewModel *> *dataSorceOfMineDownCollectionViewCellOther;

//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配置好,方便之后在需要的时候触发以及调用,代码如下:

#import "MineViewModel.h"
#import "LoginBackInfoModel.h"
#import "UIImage+YB.h"
#import "AutoLoginAPIManager.h"

@implementation MineViewModel

- (instancetype)init
{
    self = [super init];
    if (self) {

        [self initialize];

    }
    return self;
}

- (void)initialize {

    [self.autoLoginCommand.executionSignals.switchToLatest subscribeNext:^(id responds) {

        if (!responds) {

        } else if ([responds isKindOfClass:[NSString class]]) {

            [MBProgressHUD showHintAlertWithText:(NSString *)responds view:[UIApplication sharedApplication].keyWindow];

        } else {

            NSDictionary *resultDic = (NSDictionary *)responds;
            if ([resultDic[@"status"] isEqualToString:@"error"]) {

                [MBProgressHUD showHintAlertWithText:resultDic[@"error_desc"] view:[UIApplication sharedApplication].keyWindow];

            } else {
                //字典转模型
                [[LoginBackInfoModel shareLoginBackInfoModel] mj_setKeyValues:resultDic[@"auto_login"]];
            }

        }

    }];

    [[[self.autoLoginCommand.executing skip:1] take:1] subscribeNext:^(id x) {
        if ([x isEqualToNumber:@(1)]) {

        }
    }];
}

#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) {


                if (![[NSUserDefaults standardUserDefaults] objectForKey:@"token"]) {
                    [subscriber sendCompleted];
                    return nil;
                }

                NSDictionary *paramDic = @{@"act":@"auto_login", @"token":[[NSUserDefaults standardUserDefaults] objectForKey:@"token"]};

                [[AutoLoginAPIManager new] startAPIManagerWithParagmas: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;
            }
        }];
        [[[RACObserve([LoginBackInfoModel shareLoginBackInfoModel], user_mobile_mask) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) {
            if (x == nil) {
                _mineHeaderViewModel.phone = @"--";
            } else {
                _mineHeaderViewModel.phone = x;
            }
        }];
        [[[RACObserve([LoginBackInfoModel shareLoginBackInfoModel], nickname) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) {
            if (x == nil) {
                _mineHeaderViewModel.name = @"点击登录";
            } else {
                _mineHeaderViewModel.name = x;
            }
        }];

    }
    return _mineHeaderViewModel;
}
- (NSArray<MineTopCollectionViewCellViewModel *> *)dataSorceOfMineTopCollectionViewCell {

    if (!_dataSorceOfMineTopCollectionViewCell) {

        MineTopCollectionViewCellViewModel *model1 = [MineTopCollectionViewCellViewModel new];
        model1.headerImage = [UIImage imageNamed:@"wdqb_ye"];
        model1.headerTitle = @"余额";
        [[[RACObserve([LoginBackInfoModel shareLoginBackInfoModel], balance) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) {
            if (x == nil) {
                model1.content = @"--";
            } else {
                model1.content = x;
            }
        }];

        MineTopCollectionViewCellViewModel *model2 = [MineTopCollectionViewCellViewModel new];
        model2.headerImage = [UIImage imageNamed:@"redPacket"];
        model2.headerTitle = @"红包";
        [[[RACObserve([LoginBackInfoModel shareLoginBackInfoModel], coupon_num) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) {
            if (x == nil) {
                model2.content = @"--";
            } else {
                model2.content = x;
            }
        }];

        _dataSorceOfMineTopCollectionViewCell = @[model1, model2];
    }
    return _dataSorceOfMineTopCollectionViewCell;
}
- (NSArray<MineDownCollectionViewCellViewModel *> *)dataSorceOfMineDownCollectionViewCell {

    if (!_dataSorceOfMineDownCollectionViewCell) {

        NSMutableArray *modelArr = [@[] mutableCopy];
        NSArray *imageNamesArr = @[@"wodi_qb", @"wodi_dianhua", @"wodi_lishi-1"];
        NSArray *names = @[@"我的钱包", @"联系客服", @"我要合作"];
        for (int i = 0; i < imageNamesArr.count; i ++) {
            MineDownCollectionViewCellViewModel *model = [MineDownCollectionViewCellViewModel new];
            model.headerImage = [UIImage imageNamed:imageNamesArr[i]];
            model.title = names[i];
            [modelArr addObject:model];
        }

        _dataSorceOfMineDownCollectionViewCell = [NSArray arrayWithArray:modelArr];
    }
    return _dataSorceOfMineDownCollectionViewCell;
}
- (NSArray<MineDownCollectionViewCellViewModel *> *)dataSorceOfMineDownCollectionViewCellOther {

    if (!_dataSorceOfMineDownCollectionViewCellOther) {

        NSMutableArray *modelArr = [@[] mutableCopy];
        NSArray *imageNamesArr = @[@"xx_1", @"wyfk"];
        NSArray *names = @[@"帮助", @"我要反馈"];
        for (int i = 0; i < imageNamesArr.count; i ++) {
            MineDownCollectionViewCellViewModel *model = [MineDownCollectionViewCellViewModel new];
            model.headerImage = [UIImage imageNamed:imageNamesArr[i]];
            model.title = names[i];
            [modelArr addObject:model];
        }

        _dataSorceOfMineDownCollectionViewCellOther = [NSArray arrayWithArray:modelArr];
    }
    return _dataSorceOfMineDownCollectionViewCellOther;
}


@end

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

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

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

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

#import <Foundation/Foundation.h>

@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放在一块儿,结构更加清晰,维护方便。

3、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代码吧(再次强调,重在思想):

#import "MineView.h"
#import "MineHeaderCollectionReusableView.h"
#import "MineTopCollectionViewCell.h"
#import "MineDownCollectionViewCell.h"

@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 {

    __weak typeof(self) weakSelf = self;

    [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(weakSelf);
    }];

    [super updateConstraints];
}
- (void)bindViewModel {

    [self.viewModel.autoLoginCommand execute:nil];
}

#pragma mark *** UICollectionViewDataSource ***
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 4;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    switch (section) {
        case 0:
        {
            return 0;
        }
            break;
        case 1:
        {
            return self.viewModel.dataSorceOfMineTopCollectionViewCell.count;
        }
            break;
        case 2:
        {
            return self.viewModel.dataSorceOfMineDownCollectionViewCell.count;
        }
            break;
        case 3:
        {
            return self.viewModel.dataSorceOfMineDownCollectionViewCellOther.count;
        }
            break;

        default:
            return 0;
            break;
    }
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    switch (indexPath.section) {
        case 0:
        {
            return nil;
        }
            break;
        case 1:
        {
            MineTopCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineTopCollectionViewCell class])] forIndexPath:indexPath];
            cell.viewModel = self.viewModel.dataSorceOfMineTopCollectionViewCell[indexPath.row];
            return cell;
        }
            break;
        case 2:
        {
            MineDownCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineDownCollectionViewCell class])] forIndexPath:indexPath];
            cell.viewModel = self.viewModel.dataSorceOfMineDownCollectionViewCell[indexPath.row];
            return cell;
        }
            break;
        case 3:
        {
            MineDownCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineDownCollectionViewCell class])] forIndexPath:indexPath];
            cell.viewModel = self.viewModel.dataSorceOfMineDownCollectionViewCellOther[indexPath.row];
            return cell;
        }
            break;

        default:
            return nil;
            break;
    }
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
    if (kind == UICollectionElementKindSectionHeader) {

        switch (indexPath.section) {
            case 0:
            {
                MineHeaderCollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineHeaderCollectionReusableView class])] forIndexPath:indexPath];
                view.viewModel = self.viewModel.mineHeaderViewModel;
                return view;
            }
                break;

            default:
                break;
        }

    }
    return nil;
}

#pragma mark *** UICollectionViewDelegate ***
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {

    [self.viewModel.pushSubject sendNext:nil];
}

#pragma mark *** UICollectionViewDelegateFlowLayout ***
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    switch (indexPath.section) {
        case 0:
        {
            return CGSizeZero;
        }
            break;
        case 1:
        {
            return CGSizeMake(CGRectGetWidth(self.bounds) / 2.0 - 0.5, 80);
        }
            break;
        case 2:
        {
            return CGSizeMake(CGRectGetWidth(self.bounds), 44);
        }
            break;
        case 3:
        {
            return CGSizeMake(CGRectGetWidth(self.bounds), 44);
        }
            break;

        default:
            return CGSizeZero;
            break;
    }
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
    if (section == 0) {
        return CGSizeMake(CGRectGetWidth(self.bounds), 150);
    }
    return CGSizeZero;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
    switch (section) {
        case 0:
        {
            return 0;
        }
            break;
        case 1:
        {
            return 0;
        }
            break;
        case 2:
        {
            return 1;
        }
            break;
        case 3:
        {
            return 1;
        }
            break;

        default:
            return 0;
            break;
    }
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
    switch (section) {
        case 0:
        {
            return 0;
        }
            break;
        case 1:
        {
            return 1;
        }
            break;
        case 2:
        {
            return 0;
        }
            break;
        case 3:
        {
            return 0;
        }
            break;

        default:
            return 0;
            break;
    }
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
    switch (section) {
        case 0:
        {
            return UIEdgeInsetsZero;
        }
            break;
        case 1:
        {
            return UIEdgeInsetsMake(0, 0, 0, 0);
        }
            break;
        case 2:
        {
            return UIEdgeInsetsMake(10, 0, 0, 0);
        }
            break;
        case 3:
        {
            return UIEdgeInsetsMake(10, 0, 0, 0);
        }
            break;

        default:
            return UIEdgeInsetsZero;
            break;
    }
}

#pragma mark *** Getter ***
- (UICollectionView *)collectionView {
    if (!_collectionView) {

        _collectionView = [[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:[UICollectionViewFlowLayout new]];
        _collectionView.delegate = self;
        _collectionView.dataSource = self;
        _collectionView.alwaysBounceVertical = YES;
        _collectionView.backgroundColor = [UIColor clearColor];

        [_collectionView registerClass:[MineTopCollectionViewCell class] forCellWithReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineTopCollectionViewCell class])]];
        [_collectionView registerClass:[MineDownCollectionViewCell class] forCellWithReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineDownCollectionViewCell class])]];
        [_collectionView registerClass:[MineHeaderCollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineHeaderCollectionReusableView class])]];

    }
    return _collectionView;
}
- (MineViewModel *)viewModel {
    if (!_viewModel) {
        _viewModel = [[MineViewModel alloc] init];
    }
    return _viewModel;
}

@end

4、Controller

这家伙已经解放了。

#import "MineViewController.h"
#import "MineView.h"
#import "MineViewModel.h"
#import "LoginViewController.h"

@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 {

    __weak typeof(self) weakSelf = self;

    [self.mineView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(weakSelf.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

是不是非常清爽,清爽得甚至怀疑它的存在感了(~_~)。

五:附加讲述


1、绑定思想

我想,懂一些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这个方法可能见得就比较少了。

2、整体逻辑梳理

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

3、创建自己的架构

其实在任何项目中,如果某一个模块代码量太大,我们完全可以自己进行代码分离,只要遵循一定的规则(当然这是自己定义的规则),最终的目的都是让功能和业务细化,分类。

这相当于在沙滩上抓一把沙,最开始我们将石头和沙子分开,但是后来,发现沙子也有大有小,于是我们又按照沙子的大小分成两部分,再后来发现沙子颜色太多,我们又把不同颜色的沙子分开……

在MVVM模式中,完全可以把ViewModel的网络请求逻辑提出来,叫做NetworkingCenter;还可以把ViewModel中的点击等各种监听事件提出来,叫做ActionCenter;还可以把界面展示的View的各种配置(比如在tableView协议方法中的写的数据)提出来,叫做UserInterfaceConfigDataCenter;如果项目中需要处理的网络请求数据很多,我们可以将数据处理逻辑提出来,叫做DataPrecessCenter ……

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

六:结语

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

如果有空,将持续更新一些需求实现的文章(~_~)

2
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:14180次
    • 积分:316
    • 等级:
    • 排名:千里之外
    • 原创:18篇
    • 转载:1篇
    • 译文:0篇
    • 评论:4条
    最新评论