MVVM設計模式加RAC響應式編程

一:爲何要用MVVM?


爲何要用MVVM?只是由於它不會讓我時常懵逼。javascript

每次作完項目事後,都會被本身龐大的ViewController代碼嚇壞,無論是什麼網絡請求、networking data process、跳轉交互邏輯通通往ViewController裏面塞,就算是本身寫的代碼,也不敢直視。我不得不思考是否是MVC模式太過落後了,畢竟它叫作Massive View Controller,其實說MVC落後不太合理,說它太原生了比較合適。java

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

大體的瞭解一下RAC事後,即可以往下(^)模塊化

四:MVVM模塊具體實現


這是要實現的界面:

AF45BFF3B07B52D222AF90AE1CCBAC18.png

一、Model

這裏我弱化了Model的做用,它只是做爲一個網絡請求數據的中轉站,只有在View須要顯示網絡數據的時候,對應的ViewModel裏面纔有Model的相關處理。

二、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放在一起,結構更加清晰,維護方便。

三、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

四、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

是否是很是清爽,清爽得甚至懷疑它的存在感了(_)。

五:附加講述


一、綁定思想

我想,懂一些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這個方法可能見得就比較少了。

二、總體邏輯梳理

  1. 進入ViewController,懶加載初始化主View(調用-initWithViewMdoel方法,保證主ViewModel惟一性),懶加載初始化主ViewModel。
  2. 進入主ViewModel,初始化配置網絡請求、點擊邏輯、初始化各個子ViewModel。
  3. 進入主View,經過主ViewModel初始化,調用ViewModel中的對應邏輯和對應子ViewModel展現數據。
  4. ViewController與ViewModel的交互主要是跳轉邏輯等。

三、建立本身的架構

其實在任何項目中,若是某一個模塊代碼量太大,咱們徹底能夠本身進行代碼分離,只要遵循必定的規則(固然這是本身定義的規則),最終的目的都是讓功能和業務細化,分類。

這至關於在沙灘上抓一把沙,最開始咱們將石頭和沙子分開,可是後來,發現沙子也有大有小,因而咱們又按照沙子的大小分紅兩部分,再後來發現沙子顏色太多,咱們又把不一樣顏色的沙子分開……

在MVVM模式中,徹底能夠把ViewModel的網絡請求邏輯提出來,叫作NetworkingCenter;還能夠把ViewModel中的點擊等各類監聽事件提出來,叫作ActionCenter;還能夠把界面展現的View的各類配置(好比在tableView協議方法中的寫的數據)提出來,叫作UserInterfaceConfigDataCenter;若是項目中須要處理的網絡請求數據不少,咱們能夠將數據處理邏輯提出來,叫作DataPrecessCenter ……

記住一句話:萬變不離其宗。

六:結語

移動端的架構一直都是變幻無窮,沒有萬能的架構,只有萬能的程序員,根據產品的需求選擇相應的架構纔是正確的作法,MVC當然古老,可是在小型項目卻依然實用;MVVM+RAC雖然很強大,可是在有時候仍是會增長代碼量,其實MVVM和Android裏面的MVP模式有至關多的共同點,能夠借鑑瞭解;至於MVCS沒有什麼可講的,VIPER模式看起來比較厲害,想想可能又是把哪一個模塊細化了,猜想ViewModel?嘿嘿,其實我沒研究過VIPER,就不班門弄斧了。

相關文章
相關標籤/搜索