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

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

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

- ViewModel的設(shè)計(jì)
/// 登錄界面的視圖模型 -- VM
@interface SULoginViewModel1 : NSObject
/// 手機(jī)號(hào)
@property (nonatomic, readwrite, copy) NSString *mobilePhone;
/// 驗(yàn)證碼
@property (nonatomic, readwrite, copy) NSString *verifyCode;
/// 登錄按鈕的點(diǎn)擊狀態(tài)
@property (nonatomic, readonly, assign) BOOL validLogin;
/// 用戶頭像
@property (nonatomic, readonly, copy) NSString *avatarUrlString;
/// 用戶登錄 為了減少View對(duì)viewModel的狀態(tài)的監(jiān)聽(tīng) 這里采用block回調(diào)來(lái)減少狀態(tài)的處理
- (void)loginSuccess:(void(^)(id json))success
failure:(void (^)(NSError *error))failure;
@end
很明顯viewModel僅僅只暴漏了視圖控制器所必需的最小量的信息,設(shè)置readonly屬性很有必要,同時(shí),視圖控制器C實(shí)際上并不在乎 viewModel是如何獲得這些信息的。切記:ViewModel千萬(wàn)不要主動(dòng)對(duì)視圖控制器C以任何形式直接起作用或直接通告其變化,而是等待視圖控制器C來(lái)主動(dòng)獲取。
想必大家可能對(duì)下面的代碼存在疑惑,原因可能是:不是說(shuō)好的 View綁定ViewModel的呢?綁定呢?監(jiān)聽(tīng)呢?....
/// 用戶登錄 為了減少View對(duì)viewModel的狀態(tài)的監(jiān)聽(tīng) 這里采用block回調(diào)來(lái)減少狀態(tài)的處理
- (void)loginSuccess:(void(^)(id json))success
failure:(void (^)(NSError *error))failure;
對(duì)方不想和筆者說(shuō)話并向筆者扔了一個(gè)API設(shè)計(jì)
/// 是否正在執(zhí)行
@property (nonatomic, readonly, assign) BOOL executing;
/// 請(qǐng)求失敗的信息
@property (nonatomic, readonly, strong) NSError *error;
/// 請(qǐng)求成功的數(shù)據(jù)
@property (nonatomic, readonly, strong) id responseObject;
/// 調(diào)起登錄
- (void) login;
這樣設(shè)計(jì)其實(shí)也合理的,ViewController的登錄按鈕被點(diǎn)擊時(shí),調(diào)用viewModel上的login方法,同時(shí)ViewController通過(guò)KVO的方法監(jiān)聽(tīng)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ù)處理
}];
筆者不想和你說(shuō)話并向你扔了一個(gè)問(wèn)題思考。上面??一個(gè)登陸(login)操作,我們就要編寫這么多代碼,試想如果再多一個(gè)操作呢?再多兩個(gè)操作呢?.... 如果不用block回調(diào),不管你們會(huì)不會(huì),總之,我會(huì)。下面??再看看利用block的回調(diào)實(shí)現(xiàn),你們就會(huì)解惑,釋懷了,起碼好受點(diǎ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ù)將用來(lái):
- 當(dāng)
validLogin的值發(fā)生變化時(shí),觸發(fā)登錄按鈕的enabled的屬性。 - 監(jiān)聽(tīng)
avatarUrlString的變化,來(lái)更新視圖控制器的頭像的UIImageView。
- 視圖控制器對(duì)
viewModel起如下作用:
- 每當(dāng)
UITextField中的文本發(fā)生變化, 更新viewModel上的readwrite屬性mobilePhone或者verifyCode - 登錄按鈕被點(diǎn)擊時(shí),調(diào)用
viewModel上的loginSuccess:failure方法。
- 視圖控制器不要做的事
- 發(fā)起登錄的網(wǎng)絡(luò)請(qǐng)求
- 判定登錄按鈕的有效性
- 來(lái)獲取頭像的地址(PS:有可能從本地?cái)?shù)據(jù)庫(kù)獲取,也有可能通過(guò)網(wǎng)絡(luò)請(qǐng)求來(lái)獲?。?/li>
- ...
請(qǐng)?jiān)俅巫⒁庖晥D控制器總的責(zé)任是處理
viewModel中的變化。 - 視圖控制器從
五、MVVM Without ReactiveCocoa的商品首頁(yè)界面的實(shí)踐
- ViewModel的設(shè)計(jì)
/// 商品首頁(yè)的視圖模型 -- 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ù) 通過(guò)block回調(diào)減輕view 對(duì) viewModel 的狀態(tài)的監(jiān)聽(tīng)
@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(視圖控制器)
視圖控制器通過(guò)調(diào)用viewModel的loadBannerData:failure:和loadData:failure:configFooter:來(lái)獲取商品首頁(yè)的廣告數(shù)據(jù)(SUBanner)以及商品數(shù)據(jù)(SUGoods)。視圖控制器通過(guò)使用viewModel上的banners和dataSource數(shù)組中的對(duì)象來(lái)配置表格視圖(tableView)的tableViewHeader和cell。通常我們會(huì)期待展現(xiàn)dataSource的是數(shù)據(jù)-模型對(duì)象。同時(shí)你可能已經(jīng)對(duì)其感到奇怪, 因?yàn)槲覀冊(cè)噲D通過(guò)MVVM模式不暴漏數(shù)據(jù)-模型對(duì)象。 (前面提到過(guò)的)。
假設(shè)我們暴露數(shù)據(jù)-模型(SUGoods),那就分析如下:

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

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

商品模型(
SUGoods)的數(shù)據(jù)結(jié)構(gòu)如下:
/** 商品運(yùn)費(fèi)類型 */
typedef NS_ENUM(NSUInteger, SUGoodsExpressType) {
SUGoodsExpressTypeFree = 0, // 包郵
SUGoodsExpressTypeValue = 1, // 運(yùn)費(fèi)
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;
/// 是否芝麻認(rèn)證
@property (nonatomic, readwrite, assign) BOOL iszm;
@end
假設(shè)我們將數(shù)據(jù)-模型通過(guò)屬性暴露在子視圖模型的.h中,筆者將設(shè)計(jì)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è)計(jì)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;
/// 芝麻認(rèn)證
self.realNameIcon.hidden = !viewModel.goods.iszm;
/// 用戶ID
self.userIdLabel.text = viewModel.userId;
}
假設(shè)我們將數(shù)據(jù)-模型不通過(guò)屬性暴露在子視圖模型的.h中,筆者將設(shè)計(jì)SUGoodsItemViewModel.h/m大致代碼如下??:
/// SUGoodsItemViewModel.h
/// 數(shù)據(jù)-模型(SUGoods)不暴露
@interface SUGoodsItemViewModel : NSObject
/// 用戶頭像
@property (nonatomic, readonly, copy) NSString * avatar;
/// 用戶昵稱:
@property (nonatomic, readonly, copy) NSString * nickName;
/// 是否芝麻認(rèn)證
@property (nonatomic, readonly, assign) BOOL iszm;
/// 101921 PS:有時(shí)候需要通過(guò)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;
/// 是否芝麻認(rèn)證
@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è)計(jì)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;
/// 芝麻認(rèn)證
self.realNameIcon.hidden = !viewModel.iszm;
/// 用戶ID
self.userIdLabel.text = viewModel.userId;
}
首先我們發(fā)現(xiàn),如果不通過(guò)屬性暴露數(shù)據(jù)模型,SUGoodsItemViewModel跟SUGoods也太想了吧,僅僅只是用readonly代替readwirte而已!為啥吃飽了事沒(méi)飯干將其轉(zhuǎn)化成 viewModel 的工作???神經(jīng)病?。。〖词诡愃?,viewModel 讓我們限制信息只暴露給我們需要的地方, 提供額外數(shù)據(jù)轉(zhuǎn)換的屬性, 或?yàn)樘囟ǖ囊晥D計(jì)算數(shù)據(jù)。(此外,當(dāng)可以不暴露可變數(shù)據(jù)-模型對(duì)象(SUGoods)時(shí)也是極好的,因?yàn)槲覀兿M?viewModel 自己承擔(dān)起更新它們的任務(wù),而不是靠視圖或視圖控制器。)
但是日常開(kāi)發(fā)過(guò)程中筆者 強(qiáng)烈建議大家把數(shù)據(jù)模型(SUGoods)暴露在子視圖模型(SUGoodsItemViewModel)的.h中。這樣一來(lái)子視圖模型的屬性會(huì)相應(yīng)的減少,大大減少了膠水代碼的產(chǎn)生。但是可能又會(huì)有人不想說(shuō)話并向筆者拋了一個(gè)issue?。?!
既然通過(guò)屬性暴露了數(shù)據(jù)-模型(SUGoods)了,為何還要暴露一個(gè)userId的屬性?有必要嗎?很有必要?。?!
上面已經(jīng)提到過(guò)ViewModel 提供額外數(shù)據(jù)轉(zhuǎn)換的屬性, 或?yàn)樘囟ǖ囊晥D計(jì)算數(shù)據(jù)。顯然我們完全可以不暴露userId,僅僅只要我們?cè)?code>SUGoodsCell.m中這樣寫即可,根本無(wú)傷大雅是吧。
/// 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;
/// 芝麻認(rèn)證
self.realNameIcon.hidden = !viewModel.goods.iszm;
/// 用戶ID
self.userIdLabel.text =[NSString stringWithFormat:@"用戶ID:%@",viewModel.goods.userId] ;
}
對(duì)此,筆者只能微微一笑很傾城了。因?yàn)檫@個(gè)數(shù)據(jù)的屬性過(guò)于簡(jiǎn)單,僅僅只是數(shù)據(jù)的拼接,看不出viewModel的作用和強(qiáng)大。詳情見(jiàn)下面??商品運(yùn)費(fèi)Label的顯示邏輯:
/// 郵費(fèi)情況
NSString *freightExplain = nil;
SUGoodsExpressType expressType = goods.expressType;
if (expressType==SUGoodsExpressTypeFree) {
// 包郵
freightExplain = @"包郵";
}else if(expressType == SUGoodsExpressTypeValue){
// 指定運(yùn)費(fèi)
NSString *extralFee = [NSString stringWithFormat:@"運(yùn)費(fèi) ¥%@",goods.expressFee];
freightExplain = extralFee;
}else if (expressType == SUGoodsExpressTypeFeeding){
freightExplain = @"運(yùn)費(fèi)待議";
}
self.freightExplain = freightExplain;
至此,筆者相信大家都會(huì)把上面??這段代碼寫在ViewModel中,通過(guò)暴露一個(gè)只讀(readonly)的freightExplain屬性供cell獲取展示,而不是Cell中編寫這段又臭又長(zhǎng)的邏輯代碼。
六、劃重點(diǎn),漲姿勢(shì)
- 保證將
MVVM中Model設(shè)計(jì)成Thin-Model(瘦模型),避免其淪為Fat-Model(胖模型),且不要與ViewModel混淆一談,兩者道不同,不相為謀。 -
View和ViewModel之間存在數(shù)據(jù)和事件的雙向綁定的關(guān)系,利用 ReactiveCocoa 來(lái)充當(dāng)view和viewModel之間binder的角色,優(yōu)雅地實(shí)現(xiàn)兩者之間的數(shù)據(jù)綁定(同步),切記:ReactiveCocoa 并非是實(shí)現(xiàn)MVVM設(shè)計(jì)模式的充要條件。MVVM的關(guān)鍵是要有ViewModel!而不是 ReactiveCocoa -
MVVM可以看成是MVMCV的設(shè)計(jì)模式,從而引申出來(lái)Model、ViewModel、Controller以及View他們之間的角色定位,以及各自的職責(zé)所在。切勿試圖萌生用ViewModel來(lái)代替ViewController,Controller在MVVM中負(fù)責(zé)View和ViewModel之間的綁定和常規(guī)的UI邏輯處理,而ViewModel目的在于抽離ViewController中展示業(yè)務(wù)邏輯。ViewModel和ViewController在一起,但獨(dú)立。 - 在
view/viewController中不能直接引用模型Model,viewModel不必在屏幕上顯示所有東西。針對(duì)實(shí)際的業(yè)務(wù),將一組業(yè)務(wù)邏輯相關(guān)的代碼抽取到一個(gè)獨(dú)立的視圖模型中處理(子ViewModel)。 -
視圖模型可以通過(guò)屬性的方式暴露一個(gè)只讀的數(shù)據(jù)模型,視圖模型負(fù)責(zé)提供額外數(shù)據(jù)轉(zhuǎn)換的屬性, 或?yàn)樘囟ǖ囊晥D提供計(jì)算數(shù)據(jù)。為了消除View過(guò)多的觀察ViewModel的狀態(tài)(屬性)的變化,我們可以通過(guò)block的方式回調(diào)請(qǐng)求數(shù)據(jù)。
七、代碼閱讀
由于這個(gè)功能筆者分別采用 MVC和MVVM Without ReactiveCococa來(lái)開(kāi)發(fā)實(shí)踐,畢竟蘿卜白菜,各有所愛(ài),目的就是便于大家更深層次的了解MVC和MVVM的異同,以及提供一個(gè)利用MVVM Without ReactiveCococa真實(shí)開(kāi)發(fā)的樣例,希望能夠打消大家對(duì) MVVM 模式的顧慮。為了方便我們從宏觀上了解功能的的整體結(jié)構(gòu),我們可以分別看看MVC和MVVM Without RAC的類圖。大家可以跟著類圖,順藤摸瓜,秉承該看的看,不該看的偷偷看的原則,趕快行動(dòng)起來(lái)吧。
-
MVC類圖
MVC類圖.png
-
MVVM Without RAC 類圖
MVVMWithoutRAC類圖.png 源碼地址(PS: 還請(qǐng)star一下,不會(huì)懷孕??的)
MHDevelopExample_Objective_C 目錄中的 MVC&MVVM文件夾中
八、期待
- 文章若對(duì)您有點(diǎn)幫助,請(qǐng)給個(gè)喜歡??,畢竟碼字不易;若對(duì)您沒(méi)啥幫助,請(qǐng)給點(diǎn)建議??,切記學(xué)無(wú)止境。
- 針對(duì)文章所述內(nèi)容,閱讀期間任何疑問(wèn);請(qǐng)?jiān)谖恼碌撞颗u(píng)指正,我會(huì)火速解決和修正問(wèn)題。
- 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









