https://cloud.tencent.com/developer/article/1117009
一:為什么要用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的觀感是這樣的:
@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,就不班門弄斧了。