https://cloud.tencent.com/developer/article/1117009
A:なぜ使用MVVM?
なぜMVVMを使うのか?それは私に多くの場合、無知な力をさせないであろうという理由だけで。
各完成したプロジェクトの後、外観に勇気がなかった、のViewControllerをジャンプするために、すべての対話ロジックが詰め、でも自分のコードを書くのデータ処理をネットワーキング、彼の巨大なのViewControllerコードに関係なく、どのようなネットワーク要求を恐怖されます。私はそれが太原に、より適切な出産した、MVCパターンについて考えなければならなかった、すべての後に、MVCの背後に合理的ではないことを、実際には、大規模なビューコントローラと呼ばれすぎの後ろにされていません。
歴史MVCパターンは非常に古いですが、実際にはそれがMVVM、MVCS、または音不気味VIPERは、MVC標準の三つのモジュールの分割を継続しているかどうか、モジュールモードであり、セグメントダウン、そのためより多くの独立した単一の各モジュールの機能、究極の目標は、デカップリング、仕様コードの度合いを高め、メンテナンスコストを削減することですが。どのような特定のパターンは、プロジェクトのニーズによって決定される必要があり、ここで、私は単純に謙虚な意見を言えば、MVVM設計とアーキテクチャの理解について話しています。
2:MVVMモジュール部門
モデル、ビュー、コントローラ:伝統的なMVCモデルが分かれています。コントローラは、ロジックとビジネス、および1万泥の馬急いで過去の瞬時コントローラー心の残りの部分を担当しながら、モデルは脂肪、薄い点がある、データモデルで、ビューは、インターフェイスのディスプレイを担当しています。
ビューモデルとMVVMパターンしかしは、その役割はコントローラ負担であり、コントローラは、自身に転送ロジック(主に弱いビジネスロジック)の内部に、実際に、それはさらになど、ページ表示データを処理する、より多くのより動作するようになります。(以降の章で詳細に説明します)
私のデザインはこれです:
- ViewModelにビュー、ビューモデルを処理するインターフェイス要素と属性を結合ビューのデータ属性に対応
- データのネットワークを作成する必要がある場合にモデルにのみ、その役割は非常に薄いプロファイルモデルでのみ二次局データであり、
- ここでは、モデルの役割を弱体化、およびネットワーク・データの論理処理は、唯一のViewModelビューネットワークデータは、モデルは影が、データを処理した後に表示されます表示さがある、と言うことですViewModelに、中に配置されますこれらのプロパティはURL UIImageを書いていない、そのような書き込みをとして、「直感」をしようとしなければならないことのViewModelのプロパティ、ノートになります
- ViewModelおよびモデルは、ケースとしての性質を結合するための必要性を見ることができます
- コントローラの主な役割は、対応するビュービューモデルの初期化を渡すことで、その後、ビジネス・ロジック・トリガのごく一部、もちろん、まだのViewControllerがここに実現ジャンプする必要があるようなもので、その後、論理モニターをスキップし、self.viewに追加します。注:プロパティの変更は、リスナーがいくつかの論理処理を行う際に結合、実際には、プロパティを監視するために、強力なフレームワークが来た---- RACの言及があります
三:ReactiveCocoa
RACは強力なツールであり、それはMVVMパターンと組み合わせて使用されている唯一の単語----完璧に説明することができます。
もちろん、一部の開発者は、彼らはそれを知らせるために代理店を破壊感じおそらくので、これらのものを使用することに消極的モニター、ブロックや他の複雑なロジックのルックアンドフィールが、私のアイデアはMVVMの構築を伴うであろうので、私は、ここに偉大な自尊心でRACよプロパティバインディングの数が多い、イベント配信が、私はこれらの機能を実装するために千10以上の単純なプロトコルを記述する必要はありませんが、RACを使用すると、ロジックをより明確にするために多くのコードを簡素化することができます。
次に、私はあなたがRACを使用していない場合は、その前に、詳細にMVVMアーキテクチャを行うには私のアイデアを実現し、会場ください。
RACに関する一般的な理解した後、あなたのダウンすることができます(^)
4:MVVMモジュールが達成され、
これは、インターフェイスを達成すべきです。
1、モデル
ここで私はそれが唯一のネットワーク要求データのための通過点として、ネットワークにのみ表示対応のViewModelは唯一のモデルに関連する処理で表示データに必要され、モデルの役割を弱めます。
2、ViewModelに
それらの実際の開発、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、整体逻辑梳理
- 进入ViewController,懒加载初始化主View(调用-initWithViewMdoel方法,保证主ViewModel唯一性),懒加载初始化主ViewModel。
- 进入主ViewModel,初始化配置网络请求、点击逻辑、初始化各个子ViewModel。
- 进入主View,通过主ViewModel初始化,调用ViewModel中的对应逻辑和对应子ViewModel展示数据。
- ViewController与ViewModel的交互主要是跳转逻辑等。
3、创建自己的架构
其实在任何项目中,如果某一个模块代码量太大,我们完全可以自己进行代码分离,只要遵循一定的规则(当然这是自己定义的规则),最终的目的都是让功能和业务细化,分类。
这相当于在沙滩上抓一把沙,最开始我们将石头和沙子分开,但是后来,发现沙子也有大有小,于是我们又按照沙子的大小分成两部分,再后来发现沙子颜色太多,我们又把不同颜色的沙子分开……
在MVVM模式中,完全可以把ViewModel的网络请求逻辑提出来,叫做NetworkingCenter;还可以把ViewModel中的点击等各种监听事件提出来,叫做ActionCenter;还可以把界面展示的View的各种配置(比如在tableView协议方法中的写的数据)提出来,叫做UserInterfaceConfigDataCenter;如果项目中需要处理的网络请求数据很多,我们可以将数据处理逻辑提出来,叫做DataPrecessCenter ……
记住一句话:万变不离其宗。
六:结语
移动端的架构一直都是千变万化,没有万能的架构,只有万能的程序员,根据产品的需求选择相应的架构才是正确的做法,MVC固然古老,但是在小型项目却依然实用;MVVM+RAC虽然很强大,但是在有时候还是会增加代码量,其实MVVM和Android里面的MVP模式有相当多的共同点,可以借鉴了解;至于MVCS没有什么可讲的,VIPER模式看起来比较厉害,想一想可能又是把哪个模块细化了,猜测ViewModel?嘿嘿,其实我没研究过VIPER,就不班门弄斧了。