iOS Architecture: MVVM design patterns + RAC reactive programming

https://cloud.tencent.com/developer/article/1117009

A: Why use MVVM?


Why use MVVM? Just because it will not let me often ignorant force.

After each finished project, will be terrified his huge ViewController the code, no matter what network requests, networking data process, all the interaction logic to jump ViewController stuffed, even write their own code, did not dare to look. I had to think about the MVC pattern is not too far behind, after all, it is called Massive View Controller, in fact, that behind MVC is not reasonable, it gave birth to Taiyuan more appropriate.

History MVC pattern is very old, but in fact it is a modular programming mode, whether MVVM, MVCS, or sounds creepy VIPER, are continuing division of the MVC standard three modules, segments down, so that the function of each module of a more independent and single, while the ultimate goal is to enhance the degree of specification code, decoupling, and reduce maintenance costs. What specific pattern needs to be determined by the needs of the project, and here, I simply talk about their understanding of MVVM design and architecture, Talking humble opinion.

Two: MVVM module division


The traditional MVC model is divided into: Model, View, Controller. Model is the data model, there are fat, thin points, View is responsible for interface display, while the Controller is responsible for the rest of the logic and business, and instantly Controller hearts of ten thousand mud horse rushing past.

MVVM pattern but with an ViewModel, its role is to Controller burdens, the Controller inside logic (mainly weak business logic) transferred to itself, in fact, it comes to work more than that, further comprising processing the page display data, etc. . (Subsequent chapters will explain in detail)

My design is this:

  • Corresponds to a ViewModel a View, the View data attribute binding interface elements and attributes processing ViewModel
  • Model only when there is need to create a network of data, its role is only a secondary station data, which is a very thin profile model
  • Here weakened the Model role, and logic processing of network data will be placed in the ViewModel, that is to say, there are only ViewModel View network data show, the Model will see the shadow, but after processing the data, will become the property of the ViewModel, note that these properties must try "intuition", such as to write, do not write URL UIImage
  • ViewModel and Model, as the case may see the need for binding properties
  • Controller main role is to pass the corresponding View ViewModel initialization, and then added to self.view, then skip logic monitor is like a small part of the business logic trigger, of course, still need to jump ViewController realized here. Note: there is mention of the binding, in fact, to monitor the property, when the property changes, listeners do some logic processing, powerful framework came ---- RAC

三:ReactiveCocoa

RAC is a powerful tool, and it is used in conjunction with the MVVM pattern can only be described in one word ---- perfect.

Of course, some developers reluctant to use these things, presumably because they feel it destroys the agency to inform, monitor, block and other complex logic look and feel, but I'm in great esteem here RAC, because my ideas which will involve building MVVM large number of properties bindings, event delivery, I do not want to write over ten thousand simple protocol to implement these functions, the use of RAC can simplify a lot of code to make logic more clear.

Next, I will realize my idea to do a MVVM architecture in detail, before that, if you have not used the RAC, please venue:

After a general understanding about the RAC, you can down (^)

Four: MVVM module is achieved


This is to be achieved interface:

 

1、Model

Here I weaken the role of Model, it is only as a transit point for network requests data, the network only need to display data in the View when the corresponding ViewModel which only Model-related processing.

2、ViewModel

In the actual development of them, a corresponding one of the ViewModel View, View corresponds and binds a primary main ViewModel.

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

主ViewModel的观感是这样的:

@interface MineViewModel : NSObject

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

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

@implementation MineViewModel

- (instancetype)init
{
    self = [super init];
    if (self) {
        [self initialize];
    }
    return self;
}
- (void)initialize {
    [self.autoLoginCommand.executionSignals.switchToLatest subscribeNext:^(id responds) {
        //处理网络请求数据
        ......
    }];
}

#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) {
                NSDictionary *paramDic = @{......};
                [Network start: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;
            }
        }];
        ......
    return _mineHeaderViewModel;
}
- (NSArray<MineTopCollectionViewCellViewModel *> *)dataSorceOfMineTopCollectionViewCell {
    if (!_dataSorceOfMineTopCollectionViewCell) {
        MineTopCollectionViewCellViewModel *model1 = [MineTopCollectionViewCellViewModel new];
        MineTopCollectionViewCellViewModel *model2 = [MineTopCollectionViewCellViewModel new];
        ......
        _dataSorceOfMineTopCollectionViewCell = @[model1, model2];
    }
    return _dataSorceOfMineTopCollectionViewCell;
}
- (NSArray<MineDownCollectionViewCellViewModel *> *)dataSorceOfMineDownCollectionViewCell {
    if (!_dataSorceOfMineDownCollectionViewCell) {
        ......
    }
    return _dataSorceOfMineDownCollectionViewCell;
}

@end

  

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

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

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

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

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

@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 {
    [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(self);
    }];
    [super updateConstraints];
}
- (void)bindViewModel {
    [self.viewModel.autoLoginCommand execute:nil];
}

#pragma mark *** UICollectionViewDataSource ***
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 3;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    if (section == 1) return self.viewModel.dataSorceOfMineTopCollectionViewCell.count;
    ......
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 1) {
        MineTopCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineTopCollectionViewCell class])] forIndexPath:indexPath];
            cell.viewModel = self.viewModel.dataSorceOfMineTopCollectionViewCell[indexPath.row];
            return cell;
    }
    ......
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
    ......
}

#pragma mark *** UICollectionViewDelegate ***
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    [self.viewModel.pushSubject sendNext:nil];
}

#pragma mark *** UICollectionViewDelegateFlowLayout ***
    ......
#pragma mark *** Getter ***
- (UICollectionView *)collectionView {
    if (!_collectionView) {
        ......
    }
    return _collectionView;
}
- (MineViewModel *)viewModel {
    if (!_viewModel) {
        _viewModel = [[MineViewModel alloc] init];
    }
    return _viewModel;
}

@end

  

4、Controller

这家伙已经解放了。

@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 {
    [self.mineView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(self.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,就不班门弄斧了。

Guess you like

Origin www.cnblogs.com/baitongtong/p/11942288.html