iOS 關(guān)于MVVM Without ReactiveCocoa設(shè)計(jì)模式的那些事

一、概述
  • 通過(guò)上一篇文章的學(xué)習(xí),我們對(duì)關(guān)于MVC的弊端的產(chǎn)生和MVVMviewModel的職責(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ì)到MVVMM、V、VM各自的職責(zé),以及VVM之間那份剪不斷,理還亂的纏綿往事。
  • 本文只是筆者在實(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
  1. MVVM的基本概念
  • MVVM的結(jié)構(gòu)圖


    MVVM結(jié)構(gòu)圖.png
  • MVVM的定義
    從上圖中,我們可以非常清楚地看到 MVVM 中四個(gè)組件之間的關(guān)系。注:除了 view 、viewModelmodel 之外,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中拆出兩部分:ModelViewModel(PS:感覺(jué)是否有點(diǎn)道理)。
    View:MVC 中的viewcontroller 組成,負(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é)任。它是從 MVCcontroller 中抽取出來(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) viewviewModel的同步,避免編寫大量繁雜的樣板化代碼。在MVVM實(shí)現(xiàn)中,利用 ReactiveCocoa 來(lái)在viewviewModel 之間充當(dāng) binder 的角色,優(yōu)雅地實(shí)現(xiàn)兩者之間的數(shù)據(jù)綁定(同步)。
  1. 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ù)MVCMVVM的職責(zé)劃分,我們利用圖解來(lái)表示,首先我們顛倒了 MVC 中的 VC,于是首字母縮寫更能準(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)方式如下:

  ![MVMCV.png](http://upload-images.jianshu.io/upload_images/1874977-83316d550a75ca16.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  從上圖可知,`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ù)配置`和`管理各種各樣的視圖`。

所以ControllerMVVM中,一方面負(fù)責(zé)ViewViewModel之間的綁定,另一方面也負(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)ViewViewModel之間的數(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è)(筆者的需求
  • 效果圖
MVC和MVVM實(shí)踐效果圖.gif
四、MVVM Without ReactiveCocoa的登錄界面的實(shí)踐
  • 邏輯分析圖
登錄界面邏輯圖.png
  • 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)executingerror、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(視圖控制器)

    1. 視圖控制器從 viewModel獲取的數(shù)據(jù)將用來(lái):
    • 當(dāng)validLogin的值發(fā)生變化時(shí),觸發(fā)登錄按鈕enabled的屬性。
    • 監(jiān)聽(tīng)avatarUrlString的變化,來(lái)更新視圖控制器的頭像UIImageView。
    1. 視圖控制器對(duì) viewModel 起如下作用:
    • 每當(dāng) UITextField 中的文本發(fā)生變化, 更新 viewModel上的 readwrite屬性 mobilePhone或者verifyCode
    • 登錄按鈕被點(diǎn)擊時(shí),調(diào)用viewModel上的loginSuccess:failure方法。
    1. 視圖控制器不要做的事
    • 發(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)用viewModelloadBannerData:failure:loadData:failure:configFooter:來(lái)獲取商品首頁(yè)的廣告數(shù)據(jù)(SUBanner)以及商品數(shù)據(jù)(SUGoods)。視圖控制器通過(guò)使用viewModel上的bannersdataSource數(shù)組中的對(duì)象來(lái)配置表格視圖(tableView)的tableViewHeadercell。通常我們會(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),那就分析如下:

商品首頁(yè)暴露數(shù)據(jù)模型.png

我們不瞎,明顯從上圖??可以看出視圖 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ì)于ViewControllerViewModel來(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)化如下:

商品首頁(yè)子視圖.png

從上面??可知,dataSource是一個(gè)里面裝著SUGoodsItemViewModel的對(duì)象數(shù)組,在表格視圖中的 tableView: cellForRowAtIndexPath:方法中,將會(huì)從視圖控制器的viewModeldataSource中通過(guò)正確的索引獲取到子viewModel, 并把它賦值給 cell上的 viewModel屬性。

想必大家還有一個(gè)疑惑,數(shù)據(jù)-模型(SUGoods)是否要通過(guò)屬性的方式暴露在子視圖模型(SUGoodsItemViewModel)的.h文件中?
我們假設(shè)要通過(guò)SUGoodsItemViewModel來(lái)提供給SUGoodsCell展示下面??的界面的數(shù)據(jù):

商品的用戶信息.png

商品模型(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ù)模型,SUGoodsItemViewModelSUGoods也太想了吧,僅僅只是用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ì)
  • 保證將MVVMModel設(shè)計(jì)成Thin-Model(瘦模型),避免其淪為Fat-Model(胖模型),且不要與ViewModel混淆一談,兩者道不同,不相為謀。
  • ViewViewModel之間存在數(shù)據(jù)和事件的雙向綁定的關(guān)系,利用 ReactiveCocoa 來(lái)充當(dāng)viewviewModel 之間 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)ModelViewModel、Controller以及View他們之間的角色定位,以及各自的職責(zé)所在。切勿試圖萌生用ViewModel來(lái)代替ViewController,ControllerMVVM中負(fù)責(zé)ViewViewModel之間的綁定和常規(guī)的UI邏輯處理,而ViewModel目的在于抽離ViewController中展示業(yè)務(wù)邏輯。ViewModelViewController在一起,但獨(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è)功能筆者分別采用 MVCMVVM Without ReactiveCococa來(lái)開(kāi)發(fā)實(shí)踐,畢竟蘿卜白菜,各有所愛(ài),目的就是便于大家更深層次的了解MVCMVVM的異同,以及提供一個(gè)利用MVVM Without ReactiveCococa真實(shí)開(kāi)發(fā)的樣例,希望能夠打消大家對(duì) MVVM 模式的顧慮。為了方便我們從宏觀上了解功能的的整體結(jié)構(gòu),我們可以分別看看MVCMVVM Without RAC的類圖。大家可以跟著類圖,順藤摸瓜,秉承該看的看,不該看的偷偷看的原則,趕快行動(dòng)起來(lái)吧。

  • MVC類圖


    MVC類圖.png
八、期待
  1. 文章若對(duì)您有點(diǎn)幫助,請(qǐng)給個(gè)喜歡??,畢竟碼字不易;若對(duì)您沒(méi)啥幫助,請(qǐng)給點(diǎn)建議??,切記學(xué)無(wú)止境。
  2. 針對(duì)文章所述內(nèi)容,閱讀期間任何疑問(wèn);請(qǐng)?jiān)谖恼碌撞颗u(píng)指正,我會(huì)火速解決和修正問(wèn)題。
  3. GitHub地址:https://github.com/CoderMikeHe
九、參考鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、概述 筆者 強(qiáng)烈推薦 大家在閱讀本文之前,還請(qǐng)先移步閱讀?? iOS 關(guān)于MVC和MVVM設(shè)計(jì)模式的那些事 和...
    CoderMikeHe閱讀 15,840評(píng)論 28 147
  • 一、概述 在 iOS 開(kāi)發(fā)中,MVC(Model View Controller)是構(gòu)建iOS App的標(biāo)準(zhǔn)模式,...
    CoderMikeHe閱讀 27,119評(píng)論 70 347
  • 傳統(tǒng)模式下的開(kāi)發(fā)MVCMVVM基于面向協(xié)議MVP的介紹MVP實(shí)戰(zhàn)開(kāi)發(fā)說(shuō)在前面:相信就算你是個(gè)iOS新手也應(yīng)該聽(tīng)說(shuō)過(guò)...
    行走的菜譜閱讀 3,312評(píng)論 1 5
  • 白雪皚皚, 蒼松陣陣, 人跡罕至, 笨重的黑熊為了食物在雪地覓食 看到了陽(yáng)光下 雪地里 那一簇神奇的黃色 那樣柔嫩...
    海深深閱讀 544評(píng)論 0 0
  • 亨利·羅林斯說(shuō):“生命中一半是糟心的事,一半是如何解決這些糟心的事” 我說(shuō):“生命中沒(méi)有糟心的事,糟心的事都是自找...
    1860fb3b42da閱讀 267評(píng)論 0 1

友情鏈接更多精彩內(nèi)容