一、概述
- 通過上一篇文章的學習,我們對關(guān)于
MVC的弊端的產(chǎn)生和MVVM中viewModel的職責及其使用注意事項,想必都有了些許了解和認識,最起碼What is MVC ? What is MVVM ?,大家也不會感覺這是最熟悉的陌生人了吧。筆者不才,本文將著重談談MVVM在iOS開發(fā)中的實際運用,以及自身通過實踐探索出來的經(jīng)驗之談,同時希望能讓大家更加深刻體會到MVVM中M、V、VM各自的職責,以及V和VM之間那份剪不斷,理還亂的纏綿往事。 - 本文只是筆者在實踐
MVVM過程中的些許見解,在此拋磚引玉,共同探討下MVVM的實踐思路,希望能夠打消你對MVVM模式的顧慮 ,提供一點思路,少走一些彎路,填補一些細坑。文章僅供大家參考,若有不妥之處,還望不吝賜教,歡迎批評指正。 -
MVVM基礎(chǔ)知識以及其使用注意不了解的,請務必戳我?? iOS 關(guān)于MVC和MVVM設(shè)計模式的那些事
二、MVVM
- MVVM的基本概念
-
MVVM的結(jié)構(gòu)圖
MVVM結(jié)構(gòu)圖.png - MVVM的定義
從上圖中,我們可以非常清楚地看到 MVVM 中四個組件之間的關(guān)系。注:除了view、viewModel和model之外,MVVM中還有一個非常重要的隱含組件binder:
Model :和MVC中的model保持一致,完全取決于你的"偏好設(shè)置"。你可能會為model封裝一些額外的操作數(shù)據(jù)的業(yè)務邏輯,雖然蘋果是推崇你這么干的,但是筆者認為不妥,這樣很可能會導致一個胖Model的產(chǎn)生,而且胖Model相對比較難移植,胖Model隨著產(chǎn)品的迭代會更加的Fat,最終難以維護,一胖毀所有。我更傾向于把它當做一個容納表現(xiàn)數(shù)據(jù)-模型(data-model)對象信息的結(jié)構(gòu)體(瘦Model),并通過一個單獨的管理類來維護/創(chuàng)建/管理模型的統(tǒng)一邏輯,又或者可以通過使用Category來擴充業(yè)務邏輯。MVVM是基于胖Model的架構(gòu)思路建立的,然后在胖Model中拆出兩部分:Model和ViewModel(PS:感覺是否有點道理)。
View:由MVC中的view和controller組成,負責 UI 的展示,綁定viewModel中的屬性,觸發(fā)viewModel中的命令以及呈現(xiàn)由viewModel提供的數(shù)據(jù)。
View-Model: 千萬不要把它與傳統(tǒng)數(shù)據(jù)-模型結(jié)構(gòu)中模型混為一談。 它的職責之一就是作為一個表現(xiàn)視圖顯示自身所需數(shù)據(jù)的靜態(tài)模型;但它也有收集, 解釋和轉(zhuǎn)換那些數(shù)據(jù)的責任。它是從MVC的controller中抽取出來的展示邏輯,負責從model中獲取view所需的數(shù)據(jù),轉(zhuǎn)換成view可以展示的數(shù)據(jù),并暴露公開的屬性和命令供view進行綁定。
Binder:在MVVM中,聲明式的數(shù)據(jù)和命令綁定是一個隱含的約定,它可以讓開發(fā)者非常方便地實現(xiàn)view和viewModel的同步,避免編寫大量繁雜的樣板化代碼。在MVVM實現(xiàn)中,利用 ReactiveCocoa 來在view和viewModel之間充當binder的角色,優(yōu)雅地實現(xiàn)兩者之間的數(shù)據(jù)綁定(同步)。
- MVVM與MVC聯(lián)系
-
職責劃分
MVVM若按照職責來劃分的話,其根據(jù)首字母縮寫如同view-model術(shù)語一樣, 對如何使用它們進行 iOS 開發(fā)體現(xiàn)得有點不太準確。
根據(jù)MVC和MVVM的職責劃分,我們利用圖解來表示,首先我們顛倒了MVC中的V和C,于是首字母縮寫更能準確地反映出實際開發(fā)中組件間的關(guān)系方位,給我們帶來MCV。若對MVVM這么干, 將V(iew)移到VM的右邊最終成為了MVMV。很明顯,這就是我們實際開發(fā)中一貫作風(套路)。
MVC&MVVM.png- 視圖遵循區(qū)塊尺寸大致可以理解成對應它們負責的工作量。
- 請注意到
MVC中視圖控制器(C)有多大,(PS:意料之中?)。 - 可以看到我們巨大的視圖控制器和
view-model之間有大塊工作上的重合。 - 也可以看看視圖控制器在
MVVM中的足跡有多大一部分是跟視圖重合的。
ViewModel的職責
viewModel一詞的確不能充分表達其職責,無法顧名思義。很多小伙伴初次接觸MVVM設(shè)計模式時,都會卡在VM(視圖模型)的職責理解和角色定位,以及View = View+Controller的理解上,Why?!!。View Coordinator(視圖協(xié)調(diào)者)可能更好的表達viewModel的意圖。viewModel從必要的資源(數(shù)據(jù)庫,網(wǎng)絡(luò)請求等)中獲取原始數(shù)據(jù),根據(jù)視圖的展示邏輯,并處理成view (controller)的展示數(shù)據(jù)。它(通常通過屬性)暴露給視圖控制器需要知道的僅關(guān)于顯示視圖工作的信息(理想地你不會暴漏你的data-model對象)。-
ViewController的職責
如果拋開ViewController不談,突然發(fā)現(xiàn)這樣的ViewModel、Mode以及View不就是"MVC",一個以ViewModel為中心的MVC?。。∵@時,大家可能異口同聲說:Are you fucking kidding me?!。
這種理解完全是錯誤的!核心問題就在于對ViewModel角色的定位不清!基于MVVM設(shè)計思路,ViewModel存在的目的在于抽離ViewController中展示業(yè)務邏輯(PS:也就是上圖MVC中視圖控制器(C)和MVVM中的VM的重合部分),而不是替代ViewController。既然不負責視圖操作邏輯,ViewModel中就不應該存在任何View對象,更不應該存在Push/Present等視圖跳轉(zhuǎn)邏輯。
其實MVVM是一定需要Controller的參與的,雖然MVVM在一定程度上弱化了Controller的存在感,并且給Controller做了減負瘦身(PS:這難道不就是MVVM的主要目的)。我們實際上最終以MVMCV告終。Model View-Model Controller View
Controller的職責.gif
`MVVM`的正確打開方式如下:

從上圖可知,`Controller`夾在`View`和`ViewModel`之間做的其中一個主要事情就是將`View`和`ViewModel`進行綁定。在邏輯上,`Controller`知道應當展示哪個`View`,`Controller`也知道應當使用哪個`ViewModel`來提供數(shù)據(jù),然而`View`和`ViewModel`它們之間是互相不知道的,所以Controller僅關(guān)注于用 `view-model 的數(shù)據(jù)配置`和`管理各種各樣的視圖`。
所以Controller在MVVM中,一方面負責View和ViewModel之間的綁定,另一方面也負責常規(guī)的UI邏輯處理。(PS:豁然開朗了沒?柳暗花明了沒?Six Six Six...)
-
MVVM模塊層級圖
模塊層級圖.png
三、MVVM Without ReactiveCocoa功能實踐的前期準備
Talk is cheap,Show me the code。光說不練假把式,光練不說啥把式。使用 MVVM 搭配 ReactiveCocoa會很優(yōu)雅地實現(xiàn)View和ViewModel之間的數(shù)據(jù)綁定,不過它的問題在于學習成本和維護成本比較高,但是切記:MVVM的關(guān)鍵是要有ViewModel!而不是 ReactiveCocoa。
RAC 是基于 KVO 構(gòu)建的。所以也可以用 KVO 來讓View 獲取 ViewModel 的變化。但我們都知道 KVO的槽點比較多,比如使用KVO 時,既需要進行 注冊成為某個對象屬性的觀察者 ,還要在合適的時間點將自己移除 ,再加上需要 覆寫一個又臭又長的方法 ,并在方法里 判斷這次是不是自己要觀測的屬性發(fā)生了變化等。這里可以使用 Facebook 開源的 KVOController,它比較優(yōu)雅地處理了 KVO 存在的一些問題,同時又能發(fā)揮 KVO 帶來的便捷性。
這也是筆者今天要講的主題:如何不借助 ReactiveCocoa 來實現(xiàn) MVVM。Let's Do It。請注意,以下內(nèi)容只是筆者針對使用MVVM Without ReactiveCocoa 在實踐過程的心得體會以及細節(jié)處理,主要側(cè)重分析 MVVM Without ReactiveCocoa的實踐思路和邏輯處理,詳細設(shè)計還請參考源碼。 當然我也會陳述我的觀點來論證,但愿能喚起大家的共鳴,共同進步。(PS:這個Demo就是筆者目前所負責項目的冰山一角,當然歡迎大家踴躍前往AppStore下載 小閑肉-母嬰二手閑置購物平臺,僅供參考。)
- UI效果圖
| 登錄效果圖 | 首頁效果圖 |
|---|---|
![]() 登錄界面效果圖一@2x.png
|
![]() 商品首頁效果圖一@2x.png
|
![]() 登錄界面效果圖二@2x.png
|
![]() 商品首頁效果圖二@2x.png
|
- 需求分析表
| 用戶登錄需求 | 商品首頁需求 |
|---|---|
| 只有用戶輸入了手機號和驗證碼,登錄按鈕才可點擊 | 界面滾動流暢,縱享絲滑 |
| 用戶輸入的手機號必須是真實有效的 | 導航欄的樣式根據(jù)用戶的滾動而變化 |
| 驗證碼為四位有效數(shù)字 | 點擊右下角的卡通頭像,滾動頂部 |
| 當用戶輸入手機號碼時需要從本地獲取用戶頭像 | 響應商品界面上的事件處理,如商品、用戶頭像、地理位置、留言和點贊的事件處理 |
備注:右上角的填充按鈕,僅僅是減少開發(fā)者的輸入(筆者的需求) |
備注:點擊頂部搜索框,回退到列表頁(筆者的需求) |
- 效果圖

四、MVVM Without ReactiveCocoa的登錄界面的實踐
- 邏輯分析圖

- ViewModel的設(shè)計
/// 登錄界面的視圖模型 -- VM
@interface SULoginViewModel1 : NSObject
/// 手機號
@property (nonatomic, readwrite, copy) NSString *mobilePhone;
/// 驗證碼
@property (nonatomic, readwrite, copy) NSString *verifyCode;
/// 登錄按鈕的點擊狀態(tài)
@property (nonatomic, readonly, assign) BOOL validLogin;
/// 用戶頭像
@property (nonatomic, readonly, copy) NSString *avatarUrlString;
/// 用戶登錄 為了減少View對viewModel的狀態(tài)的監(jiān)聽 這里采用block回調(diào)來減少狀態(tài)的處理
- (void)loginSuccess:(void(^)(id json))success
failure:(void (^)(NSError *error))failure;
@end
很明顯viewModel僅僅只暴漏了視圖控制器所必需的最小量的信息,設(shè)置readonly屬性很有必要,同時,視圖控制器C實際上并不在乎 viewModel是如何獲得這些信息的。切記:ViewModel千萬不要主動對視圖控制器C以任何形式直接起作用或直接通告其變化,而是等待視圖控制器C來主動獲取。
想必大家可能對下面的代碼存在疑惑,原因可能是:不是說好的 View綁定ViewModel的呢?綁定呢?監(jiān)聽呢?....
/// 用戶登錄 為了減少View對viewModel的狀態(tài)的監(jiān)聽 這里采用block回調(diào)來減少狀態(tài)的處理
- (void)loginSuccess:(void(^)(id json))success
failure:(void (^)(NSError *error))failure;
對方不想和筆者說話并向筆者扔了一個API設(shè)計
/// 是否正在執(zhí)行
@property (nonatomic, readonly, assign) BOOL executing;
/// 請求失敗的信息
@property (nonatomic, readonly, strong) NSError *error;
/// 請求成功的數(shù)據(jù)
@property (nonatomic, readonly, strong) id responseObject;
/// 調(diào)起登錄
- (void) login;
這樣設(shè)計其實也合理的,ViewController的登錄按鈕被點擊時,調(diào)用viewModel上的login方法,同時ViewController通過KVO的方法監(jiān)聽executing、error、responseObject的屬性即可,代碼大致如下:
_KVOController = [FBKVOController controllerWithObserver:self];
@weakify(self);
/// binding self.viewModel.executing
[_KVOController mh_observe:self.viewModel keyPath:@"executing" block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
@strongify(self);
/// 根據(jù)executing的值,控制 HUD的顯示和隱藏
if([change[NSKeyValueChangeNewKey] boolValue])
{
[MBProgressHUD mh_showProgressHUD:@"Loading..."];
}else{
[MBProgressHUD mh_hideHUD];
}
}];
/// binding self.viewModel.responseObject
[_KVOController mh_observe:self.viewModel keyPath:@"responseObject" block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
@strongify(self);
/// 成功的數(shù)據(jù)處理
}];
/// binding self.viewModel.error
[_KVOController mh_observe:self.viewModel keyPath:@"error" block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
@strongify(self);
/// 失敗的數(shù)據(jù)處理
}];
筆者不想和你說話并向你扔了一個問題思考。上面??一個登陸(login)操作,我們就要編寫這么多代碼,試想如果再多一個操作呢?再多兩個操作呢?.... 如果不用block回調(diào),不管你們會不會,總之,我會。下面??再看看利用block的回調(diào)實現(xiàn),你們就會解惑,釋懷了,起碼好受點。
[MBProgressHUD mh_showProgressHUD:@"Loading..."];
@weakify(self);
[self.viewModel loginSuccess:^(id json) {
@strongify(self);
[MBProgressHUD mh_hideHUD];
/// 成功的數(shù)據(jù)處理
} failure:^(NSError *error) {
/// 失敗的數(shù)據(jù)處理
}];
-
ViewController(視圖控制器)
- 視圖控制器從
viewModel獲取的數(shù)據(jù)將用來:
- 當
validLogin的值發(fā)生變化時,觸發(fā)登錄按鈕的enabled的屬性。 - 監(jiān)聽
avatarUrlString的變化,來更新視圖控制器的頭像的UIImageView。
- 視圖控制器對
viewModel起如下作用:
- 每當
UITextField中的文本發(fā)生變化, 更新viewModel上的readwrite屬性mobilePhone或者verifyCode - 登錄按鈕被點擊時,調(diào)用
viewModel上的loginSuccess:failure方法。
- 視圖控制器不要做的事
- 發(fā)起登錄的網(wǎng)絡(luò)請求
- 判定登錄按鈕的有效性
- 來獲取頭像的地址(PS:有可能從本地數(shù)據(jù)庫獲取,也有可能通過網(wǎng)絡(luò)請求來獲?。?/li>
- ...
請再次注意視圖控制器總的責任是處理
viewModel中的變化。 - 視圖控制器從
五、MVVM Without ReactiveCocoa的商品首頁界面的實踐
- ViewModel的設(shè)計
/// 商品首頁的視圖模型 -- VM
@interface SUGoodsViewModel1 : NSObject
/// banners
@property (nonatomic, readonly, copy) NSArray <NSString *> *banners;
/// The data source of table view.
@property (nonatomic, readwrite, strong) NSMutableArray *dataSource;
/// load banners data
- (void)loadBannerData:(void (^)(id responseObject))success
failure:(void (^)(NSError *))failure;
/**
* 加載網(wǎng)絡(luò)數(shù)據(jù) 通過block回調(diào)減輕view 對 viewModel 的狀態(tài)的監(jiān)聽
@param success 成功的回調(diào)
@param failure 失敗的回調(diào)
@param configFooter 底部刷新控件的狀態(tài) lastPage = YES ,底部刷新控件hidden,反之,show
*/
- (void)loadData:(void(^)(id json))success
failure:(void(^)(NSError *error))failure
configFooter:(void(^)(BOOL isLastPage))configFooter;
@end
-
ViewController(視圖控制器)
視圖控制器通過調(diào)用viewModel的loadBannerData:failure:和loadData:failure:configFooter:來獲取商品首頁的廣告數(shù)據(jù)(SUBanner)以及商品數(shù)據(jù)(SUGoods)。視圖控制器通過使用viewModel上的banners和dataSource數(shù)組中的對象來配置表格視圖(tableView)的tableViewHeader和cell。通常我們會期待展現(xiàn)dataSource的是數(shù)據(jù)-模型對象。同時你可能已經(jīng)對其感到奇怪, 因為我們試圖通過MVVM模式不暴漏數(shù)據(jù)-模型對象。 (前面提到過的)。
假設(shè)我們暴露數(shù)據(jù)-模型(SUGoods),那就分析如下:

我們不瞎,明顯從上圖??可以看出視圖 SUGoodsCell直接引用了模型SUGoods,這就有悖了MVVM的初衷:** view和 view controller 都不能直接引用model,而是引用視圖模型(viewModel) **
-
子ViewModel
我們必須明確:viewModel不必在屏幕上顯示所有東西。在工作中如果遇到量級非常重的控制器,可以針對實際的業(yè)務,將一組業(yè)務邏輯相關(guān)的代碼抽取到一個獨立的視圖模型中處理。你可用
子viewModel來代表屏幕上更小的、更潛在的被封裝的部分。
一般來說,viewController可以帶一個viewModel,那如果出現(xiàn)Cell時怎么辦,Cell里又包含了按鈕,按鈕又需要數(shù)據(jù)請求又怎么處理?這些都是比較常見的場景,也可以通過MVVM來解決。
我們知道viewModel的職責是為view提供數(shù)據(jù)支持,Cell也是一個View,那么為Cell配備一個viewModel不就可以了么。所以相對于ViewController的ViewModel來說,Cell上配備的viewModel就是子viewModel。
你不總是需要子viewModel。 比如,筆者可能用表格tableHeaderView視圖來渲染簡單的頁面展示。它不是個可重用的組件,所以筆者可能僅將我們已經(jīng)給視圖控制器用過的相同的viewModel傳給那個自定義的header視圖。它會用到viewModel中它需要的信息,而無視余下的部分。
針對上面??發(fā)現(xiàn)的問題,筆者優(yōu)化如下:

從上面??可知,
dataSource是一個里面裝著SUGoodsItemViewModel的對象數(shù)組,在表格視圖中的 tableView: cellForRowAtIndexPath:方法中,將會從視圖控制器的viewModel的dataSource中通過正確的索引獲取到子viewModel, 并把它賦值給 cell上的 viewModel屬性。
想必大家還有一個疑惑,數(shù)據(jù)-模型(SUGoods)是否要通過屬性的方式暴露在子視圖模型(SUGoodsItemViewModel)的.h文件中?
我們假設(shè)要通過SUGoodsItemViewModel來提供給SUGoodsCell展示下面??的界面的數(shù)據(jù):

商品模型(
SUGoods)的數(shù)據(jù)結(jié)構(gòu)如下:
/** 商品運費類型 */
typedef NS_ENUM(NSUInteger, SUGoodsExpressType) {
SUGoodsExpressTypeFree = 0, // 包郵
SUGoodsExpressTypeValue = 1, // 運費
SUGoodsExpressTypeFeeding = 2,// 待議
};
@interface SUGoods : SUModel
/// === 商品相關(guān)的屬性 ===
....
/// === 商品中的用戶相關(guān)的信息 ===
/// 用戶ID
@property (nonatomic, readwrite, copy) NSString * userId;
/// 用戶頭像
@property (nonatomic, readwrite, copy) NSString * avatar;
/// 用戶昵稱:
@property (nonatomic, readwrite, copy) NSString * nickName;
/// 是否芝麻認證
@property (nonatomic, readwrite, assign) BOOL iszm;
@end
假設(shè)我們將數(shù)據(jù)-模型通過屬性暴露在子視圖模型的.h中,筆者將設(shè)計SUGoodsItemViewModel.h/m大致代碼如下??:
/// SUGoodsItemViewModel.h
/// 數(shù)據(jù)-模型(SUGoods)以屬性的方式暴露
@interface SUGoodsItemViewModel : NSObject
/// 商品模型
@property (nonatomic, readonly, strong) SUGoods *goods;
/// 用戶ID:101921
@property (nonatomic, readonly, copy) NSString * userId;
/// 初始化
- (instancetype)initWithGoods:(SUGoods *)goods;
@end
/// SUGoodsItemViewModel.m
@interface SUGoodsItemViewModel ()
/// 商品模型
@property (nonatomic, readwrite, strong) SUGoods *goods;
/// 用戶id
@property (nonatomic, readwrite, copy) NSString *userId;
@end
@implementation SUGoodsItemViewModel
- (instancetype)initWithGoods:(SUGoods *)goods
{
self = [super init];
if (self) {
self.goods = goods;
self.userId = [NSString stringWithFormat:@"用戶ID:%@",goods.userId]
}
return self;
}
筆者將設(shè)計SUGoodsCell.m大致代碼如下??:
/// SUGoodsCell.m
- (void)bindViewModel:(SUGoodsItemViewModel *)viewModel
{
self.viewModel = viewModel;
/// 頭像
[MHWebImageTool setImageWithURL:viewModel.goods.avatar placeholderImage:placeholderUserIcon() imageView:self.userHeadImageView];
/// 昵稱
self.userNameLabel.text = viewModel.goods.nickName;
/// 芝麻認證
self.realNameIcon.hidden = !viewModel.goods.iszm;
/// 用戶ID
self.userIdLabel.text = viewModel.userId;
}
假設(shè)我們將數(shù)據(jù)-模型不通過屬性暴露在子視圖模型的.h中,筆者將設(shè)計SUGoodsItemViewModel.h/m大致代碼如下??:
/// SUGoodsItemViewModel.h
/// 數(shù)據(jù)-模型(SUGoods)不暴露
@interface SUGoodsItemViewModel : NSObject
/// 用戶頭像
@property (nonatomic, readonly, copy) NSString * avatar;
/// 用戶昵稱:
@property (nonatomic, readonly, copy) NSString * nickName;
/// 是否芝麻認證
@property (nonatomic, readonly, assign) BOOL iszm;
/// 101921 PS:有時候需要通過user_id跳轉(zhuǎn)到用戶信息的界面
@property (nonatomic, readonly, copy) NSString * user_id;
/// 用戶ID:101921
@property (nonatomic, readonly, copy) NSString * userId;
/// 初始化
- (instancetype)initWithGoods:(SUGoods *)goods;
@end
/// SUGoodsItemViewModel.m
@interface SUGoodsItemViewModel ()
/// 商品模型
@property (nonatomic, readwrite, strong) SUGoods *goods;
/// 用戶ID
@property (nonatomic, readwrite, copy) NSString * userId;
/// 用戶頭像
@property (nonatomic, readwrite, copy) NSString * avatar;
/// 用戶昵稱:
@property (nonatomic, readwrite, copy) NSString * nickName;
/// 是否芝麻認證
@property (nonatomic, readwrite, assign) BOOL iszm;
@end
@implementation SUGoodsItemViewModel
- (instancetype)initWithGoods:(SUGoods *)goods
{
self = [super init];
if (self) {
self.goods = goods;
self.userId = [NSString stringWithFormat:@"用戶ID:%@",goods.userId]
self.user_id = goods.userId;
self.nickName = goods.nickName;
self.avatar = goods.avatar;
self.iszm = goods.iszm;
}
return self;
}
筆者將設(shè)計SUGoodsCell.m大致代碼如下??:
/// SUGoodsCell.m
- (void)bindViewModel:(SUGoodsItemViewModel *)viewModel
{
self.viewModel = viewModel;
/// 頭像
[MHWebImageTool setImageWithURL:viewModel.avatar placeholderImage:placeholderUserIcon() imageView:self.userHeadImageView];
/// 昵稱
self.userNameLabel.text = viewModel.nickName;
/// 芝麻認證
self.realNameIcon.hidden = !viewModel.iszm;
/// 用戶ID
self.userIdLabel.text = viewModel.userId;
}
首先我們發(fā)現(xiàn),如果不通過屬性暴露數(shù)據(jù)模型,SUGoodsItemViewModel跟SUGoods也太想了吧,僅僅只是用readonly代替readwirte而已!為啥吃飽了事沒飯干將其轉(zhuǎn)化成 viewModel 的工作?。可窠?jīng)病?。?!即使類似,viewModel 讓我們限制信息只暴露給我們需要的地方, 提供額外數(shù)據(jù)轉(zhuǎn)換的屬性, 或為特定的視圖計算數(shù)據(jù)。(此外,當可以不暴露可變數(shù)據(jù)-模型對象(SUGoods)時也是極好的,因為我們希望 viewModel 自己承擔起更新它們的任務,而不是靠視圖或視圖控制器。)
但是日常開發(fā)過程中筆者 強烈建議大家把數(shù)據(jù)模型(SUGoods)暴露在子視圖模型(SUGoodsItemViewModel)的.h中。這樣一來子視圖模型的屬性會相應的減少,大大減少了膠水代碼的產(chǎn)生。但是可能又會有人不想說話并向筆者拋了一個issue?。?!
既然通過屬性暴露了數(shù)據(jù)-模型(SUGoods)了,為何還要暴露一個userId的屬性?有必要嗎?很有必要?。?!
上面已經(jīng)提到過ViewModel 提供額外數(shù)據(jù)轉(zhuǎn)換的屬性, 或為特定的視圖計算數(shù)據(jù)。顯然我們完全可以不暴露userId,僅僅只要我們在SUGoodsCell.m中這樣寫即可,根本無傷大雅是吧。
/// SUGoodsCell.m
- (void)bindViewModel:(SUGoodsItemViewModel *)viewModel
{
self.viewModel = viewModel;
/// 頭像
[MHWebImageTool setImageWithURL:viewModel.goods.avatar placeholderImage:placeholderUserIcon() imageView:self.userHeadImageView];
/// 昵稱
self.userNameLabel.text = viewModel.goods.nickName;
/// 芝麻認證
self.realNameIcon.hidden = !viewModel.goods.iszm;
/// 用戶ID
self.userIdLabel.text =[NSString stringWithFormat:@"用戶ID:%@",viewModel.goods.userId] ;
}
對此,筆者只能微微一笑很傾城了。因為這個數(shù)據(jù)的屬性過于簡單,僅僅只是數(shù)據(jù)的拼接,看不出viewModel的作用和強大。詳情見下面??商品運費Label的顯示邏輯:
/// 郵費情況
NSString *freightExplain = nil;
SUGoodsExpressType expressType = goods.expressType;
if (expressType==SUGoodsExpressTypeFree) {
// 包郵
freightExplain = @"包郵";
}else if(expressType == SUGoodsExpressTypeValue){
// 指定運費
NSString *extralFee = [NSString stringWithFormat:@"運費 ¥%@",goods.expressFee];
freightExplain = extralFee;
}else if (expressType == SUGoodsExpressTypeFeeding){
freightExplain = @"運費待議";
}
self.freightExplain = freightExplain;
至此,筆者相信大家都會把上面??這段代碼寫在ViewModel中,通過暴露一個只讀(readonly)的freightExplain屬性供cell獲取展示,而不是Cell中編寫這段又臭又長的邏輯代碼。
六、劃重點,漲姿勢
- 保證將
MVVM中Model設(shè)計成Thin-Model(瘦模型),避免其淪為Fat-Model(胖模型),且不要與ViewModel混淆一談,兩者道不同,不相為謀。 -
View和ViewModel之間存在數(shù)據(jù)和事件的雙向綁定的關(guān)系,利用 ReactiveCocoa 來充當view和viewModel之間binder的角色,優(yōu)雅地實現(xiàn)兩者之間的數(shù)據(jù)綁定(同步),切記:ReactiveCocoa 并非是實現(xiàn)MVVM設(shè)計模式的充要條件。MVVM的關(guān)鍵是要有ViewModel!而不是 ReactiveCocoa -
MVVM可以看成是MVMCV的設(shè)計模式,從而引申出來Model、ViewModel、Controller以及View他們之間的角色定位,以及各自的職責所在。切勿試圖萌生用ViewModel來代替ViewController,Controller在MVVM中負責View和ViewModel之間的綁定和常規(guī)的UI邏輯處理,而ViewModel目的在于抽離ViewController中展示業(yè)務邏輯。ViewModel和ViewController在一起,但獨立。 - 在
view/viewController中不能直接引用模型Model,viewModel不必在屏幕上顯示所有東西。針對實際的業(yè)務,將一組業(yè)務邏輯相關(guān)的代碼抽取到一個獨立的視圖模型中處理(子ViewModel)。 -
視圖模型可以通過屬性的方式暴露一個只讀的數(shù)據(jù)模型,視圖模型負責提供額外數(shù)據(jù)轉(zhuǎn)換的屬性, 或為特定的視圖提供計算數(shù)據(jù)。為了消除View過多的觀察ViewModel的狀態(tài)(屬性)的變化,我們可以通過block的方式回調(diào)請求數(shù)據(jù)。
七、代碼閱讀
由于這個功能筆者分別采用 MVC和MVVM Without ReactiveCococa來開發(fā)實踐,畢竟蘿卜白菜,各有所愛,目的就是便于大家更深層次的了解MVC和MVVM的異同,以及提供一個利用MVVM Without ReactiveCococa真實開發(fā)的樣例,希望能夠打消大家對 MVVM 模式的顧慮。為了方便我們從宏觀上了解功能的的整體結(jié)構(gòu),我們可以分別看看MVC和MVVM Without RAC的類圖。大家可以跟著類圖,順藤摸瓜,秉承該看的看,不該看的偷偷看的原則,趕快行動起來吧。
-
MVC類圖
MVC類圖.png
-
MVVM Without RAC 類圖
MVVMWithoutRAC類圖.png 源碼地址(PS: 還請star一下,不會懷孕??的)
MHDevelopExample_Objective_C 目錄中的 MVC&MVVM文件夾中
八、期待
- 文章若對您有點幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學無止境。
- 針對文章所述內(nèi)容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。
- GitHub地址:https://github.com/CoderMikeHe
九、參考鏈接
- http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/ ?? 譯文
- http://blog.leichunfeng.com/blog/2016/02/27/mvvm-with-reactivecocoa/
- https://github.com/leichunfeng/MVVMReactiveCocoa
- https://casatwy.com/iosying-yong-jia-gou-tan-viewceng-de-zu-zhi-he-diao-yong-fang-an.html
- http://www.cocoachina.com/ios/20160520/16004.html
- http://www.cocoachina.com/ios/20151020/13795.html
- http://draveness.me/kvocontroller.html









