iOS 關于MVVM Without ReactiveCocoa設計模式的那些事

一、概述
  • 通過上一篇文章的學習,我們對關于MVC的弊端的產生和MVVMviewModel的職責及其使用注意事項,想必都有了些許了解和認識,最起碼What is MVC ? What is MVVM ?,大家也不會感覺這是最熟悉的陌生人了吧。筆者不才,本文將著重談談MVVM在iOS開發中的實際運用,以及自身通過實踐探索出來的經驗之談,同時希望能讓大家更加深刻體會到MVVMMVVM各自的職責,以及VVM之間那份剪不斷,理還亂的纏綿往事。
  • 本文只是筆者在實踐MVVM過程中的些許見解,在此拋磚引玉,共同探討下 MVVM 的實踐思路,希望能夠打消你對 MVVM 模式的顧慮 ,提供一點思路,少走一些彎路,填補一些細坑。文章僅供大家參考,若有不妥之處,還望不吝賜教,歡迎批評指正。
  • MVVM基礎知識以及其使用注意不了解的,請務必戳我?? iOS 關于MVC和MVVM設計模式的那些事
二、MVVM
  1. MVVM的基本概念
  • MVVM的結構圖


    MVVM結構圖.png
  • MVVM的定義
    從上圖中,我們可以非常清楚地看到 MVVM 中四個組件之間的關系。注:除了 viewviewModelmodel 之外,MVVM 中還有一個非常重要的隱含組件 binder
    Model :MVC中的model保持一致,完全取決于你的"偏好設置"。你可能會為model封裝一些額外的操作數據的業務邏輯,雖然蘋果是推崇你這么干的,但是筆者認為不妥,這樣很可能會導致一個胖Model的產生,而且胖Model相對比較難移植胖Model隨著產品的迭代會更加的Fat,最終難以維護,一胖毀所有。我更傾向于把它當做一個容納表現數據-模型(data-model)對象信息的結構體(瘦Model),并通過一個單獨的管理類來維護/創建/管理模型的統一邏輯,又或者可以通過使用Category來擴充業務邏輯。MVVM是基于胖Model的架構思路建立的,然后在胖Model中拆出兩部分:ModelViewModel(PS:感覺是否有點道理)。
    View:MVC 中的viewcontroller 組成,負責 UI 的展示,綁定 viewModel中的屬性,觸發 viewModel 中的命令以及呈現由viewModel提供的數據。
    View-Model: 千萬不要把它與傳統數據-模型結構中模型混為一談。 它的職責之一就是作為一個表現視圖顯示自身所需數據的靜態模型;但它也有收集, 解釋和轉換那些數據的責任。它是從 MVCcontroller 中抽取出來的展示邏輯,負責從 model中獲取 view 所需的數據,轉換成 view可以展示的數據,并暴露公開的屬性和命令供 view 進行綁定。
    Binder:MVVM 中,聲明式的數據和命令綁定是一個隱含的約定,它可以讓開發者非常方便地實現 viewviewModel的同步,避免編寫大量繁雜的樣板化代碼。在MVVM實現中,利用 ReactiveCocoa 來在viewviewModel 之間充當 binder 的角色,優雅地實現兩者之間的數據綁定(同步)。
  1. MVVM與MVC聯系
  • 職責劃分
    MVVM若按照職責來劃分的話,其根據首字母縮寫如同 view-model術語一樣, 對如何使用它們進行 iOS 開發體現得有點不太準確。
    根據MVCMVVM的職責劃分,我們利用圖解來表示,首先我們顛倒了 MVC 中的 VC,于是首字母縮寫更能準確地反映出實際開發中組件間的關系方位,給我們帶來MCV。若對MVVM這么干, 將V(iew)移到VM的右邊最終成為了 MVMV。很明顯,這就是我們實際開發中一貫作風(套路)。

    MVC&MVVM.png

    • 視圖遵循區塊尺寸大致可以理解成對應它們負責的工作量。
    • 請注意到MVC中視圖控制器(C)有多大,(PS:意料之中?)。
    • 可以看到我們巨大的視圖控制器和 view-model 之間有大塊工作上的重合。
    • 也可以看看視圖控制器在 MVVM 中的足跡有多大一部分是跟視圖重合的。
  • ViewModel的職責
    viewModel一詞的確不能充分表達其職責,無法顧名思義。很多小伙伴初次接觸MVVM設計模式時,都會卡在VM(視圖模型)的職責理解和角色定位,以及 View = View+Controller的理解上,Why?!!View Coordinator(視圖協調者)可能更好的表達viewModel的意圖。viewModel從必要的資源(數據庫,網絡請求等)中獲取原始數據,根據視圖的展示邏輯,并處理成 view (controller)的展示數據。它(通常通過屬性)暴露給視圖控制器需要知道的僅關于顯示視圖工作的信息(理想地你不會暴漏你的 data-model對象)。

  • ViewController的職責
    如果拋開ViewController不談,突然發現這樣的ViewModelMode以及View不就是"MVC",一個以ViewModel為中心的MVC!!!這時,大家可能異口同聲說:Are you fucking kidding me?!
    這種理解完全是錯誤的!核心問題就在于對ViewModel角色的定位不清!基于MVVM設計思路,ViewModel存在的目的在于抽離ViewController中展示業務邏輯(PS:也就是上圖MVC中視圖控制器(C)和MVVM中的VM的重合部分),而不是替代ViewController。既然不負責視圖操作邏輯,ViewModel中就不應該存在任何View對象,更不應該存在Push/Present等視圖跳轉邏輯。
    其實MVVM是一定需要Controller的參與的,雖然MVVM在一定程度上弱化了Controller的存在感,并且給Controller做了減負瘦身(PS:這難道不就是MVVM的主要目的)。我們實際上最終以 MVMCV 告終。Model View-Model Controller View

    Controller的職責.gif

`MVVM`的正確打開方式如下:

  ![MVMCV.png](http://upload-images.jianshu.io/upload_images/1874977-83316d550a75ca16.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  從上圖可知,`Controller`夾在`View`和`ViewModel`之間做的其中一個主要事情就是將`View`和`ViewModel`進行綁定。在邏輯上,`Controller`知道應當展示哪個`View`,`Controller`也知道應當使用哪個`ViewModel`來提供數據,然而`View`和`ViewModel`它們之間是互相不知道的,所以Controller僅關注于用 `view-model 的數據配置`和`管理各種各樣的視圖`。

所以ControllerMVVM中,一方面負責ViewViewModel之間的綁定,另一方面也負責常規的UI邏輯處理。(PS:豁然開朗了沒?柳暗花明了沒?Six Six Six...)

  • MVVM模塊層級圖


    模塊層級圖.png
三、MVVM Without ReactiveCocoa功能實踐的前期準備

Talk is cheap,Show me the code。光說不練假把式,光練不說啥把式。使用 MVVM 搭配 ReactiveCocoa會很優雅地實現ViewViewModel之間的數據綁定,不過它的問題在于學習成本和維護成本比較高,但是切記:MVVM的關鍵是要有ViewModel!而不是 ReactiveCocoa
RAC 是基于 KVO 構建的。所以也可以用 KVO 來讓View 獲取 ViewModel 的變化。但我們都知道 KVO的槽點比較多,比如使用KVO 時,既需要進行 注冊成為某個對象屬性的觀察者 ,還要在合適的時間點將自己移除 ,再加上需要 覆寫一個又臭又長的方法 ,并在方法里 判斷這次是不是自己要觀測的屬性發生了變化等。這里可以使用 Facebook 開源的 KVOController,它比較優雅地處理了 KVO 存在的一些問題,同時又能發揮 KVO 帶來的便捷性。
這也是筆者今天要講的主題:如何不借助 ReactiveCocoa 來實現 MVVM。Let's Do It。請注意,以下內容只是筆者針對使用MVVM Without ReactiveCocoa 在實踐過程的心得體會以及細節處理,主要側重分析 MVVM Without ReactiveCocoa的實踐思路和邏輯處理,詳細設計還請參考源碼。 當然我也會陳述我的觀點來論證,但愿能喚起大家的共鳴,共同進步。(PS:這個Demo就是筆者目前所負責項目的冰山一角,當然歡迎大家踴躍前往AppStore下載 小閑肉-母嬰二手閑置購物平臺,僅供參考。)

  • UI效果圖
登錄效果圖 首頁效果圖
登錄界面效果圖一@2x.png
商品首頁效果圖一@2x.png
登錄界面效果圖二@2x.png
商品首頁效果圖二@2x.png
  • 需求分析表
用戶登錄需求 商品首頁需求
只有用戶輸入了手機號和驗證碼,登錄按鈕才可點擊 界面滾動流暢,縱享絲滑
用戶輸入的手機號必須是真實有效的 導航欄的樣式根據用戶的滾動而變化
驗證碼為四位有效數字 點擊右下角的卡通頭像,滾動頂部
當用戶輸入手機號碼時需要從本地獲取用戶頭像 響應商品界面上的事件處理,如商品、用戶頭像、地理位置、留言和點贊的事件處理
備注:右上角的填充按鈕,僅僅是減少開發者的輸入(筆者的需求 備注:點擊頂部搜索框,回退到列表頁(筆者的需求
  • 效果圖
MVC和MVVM實踐效果圖.gif
四、MVVM Without ReactiveCocoa的登錄界面的實踐
  • 邏輯分析圖
登錄界面邏輯圖.png
  • ViewModel的設計
/// 登錄界面的視圖模型 -- VM
@interface SULoginViewModel1 : NSObject
/// 手機號
@property (nonatomic, readwrite, copy) NSString *mobilePhone;
/// 驗證碼
@property (nonatomic, readwrite, copy) NSString *verifyCode;
/// 登錄按鈕的點擊狀態
@property (nonatomic, readonly, assign) BOOL validLogin;
/// 用戶頭像
@property (nonatomic, readonly, copy) NSString *avatarUrlString;
/// 用戶登錄 為了減少View對viewModel的狀態的監聽 這里采用block回調來減少狀態的處理
- (void)loginSuccess:(void(^)(id json))success
         failure:(void (^)(NSError *error))failure;
@end

很明顯viewModel僅僅只暴漏了視圖控制器所必需的最小量的信息,設置readonly屬性很有必要,同時,視圖控制器C實際上并不在乎 viewModel是如何獲得這些信息的。切記:ViewModel千萬不要主動對視圖控制器C以任何形式直接起作用或直接通告其變化,而是等待視圖控制器C來主動獲取。
想必大家可能對下面的代碼存在疑惑,原因可能是:不是說好的 View綁定ViewModel的呢?綁定呢?監聽呢?....

/// 用戶登錄 為了減少View對viewModel的狀態的監聽 這里采用block回調來減少狀態的處理
- (void)loginSuccess:(void(^)(id json))success
         failure:(void (^)(NSError *error))failure;

對方不想和筆者說話并向筆者扔了一個API設計

/// 是否正在執行
@property (nonatomic, readonly, assign) BOOL executing;
/// 請求失敗的信息
@property (nonatomic, readonly, strong) NSError *error;
/// 請求成功的數據
@property (nonatomic, readonly, strong) id responseObject;
/// 調起登錄
- (void) login;

這樣設計其實也合理的,ViewController登錄按鈕被點擊時,調用viewModel上的login方法,同時ViewController通過KVO的方法監聽executingerrorresponseObject的屬性即可,代碼大致如下:

_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);
       /// 根據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);
        /// 成功的數據處理
}];

/// 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);
        /// 失敗的數據處理
}];

筆者不想和你說話并向你扔了一個問題思考。上面??一個登陸(login)操作,我們就要編寫這么多代碼,試想如果再多一個操作呢?再多兩個操作呢?.... 如果不用block回調,不管你們會不會,總之,我會。下面??再看看利用block的回調實現,你們就會解惑,釋懷了,起碼好受點。

[MBProgressHUD mh_showProgressHUD:@"Loading..."];
@weakify(self);
[self.viewModel loginSuccess:^(id json) {
    @strongify(self);
    [MBProgressHUD mh_hideHUD];
    /// 成功的數據處理
} failure:^(NSError *error) {
   /// 失敗的數據處理
}];
  • ViewController(視圖控制器)

    1. 視圖控制器從 viewModel獲取的數據將用來:
    • validLogin的值發生變化時,觸發登錄按鈕enabled的屬性。
    • 監聽avatarUrlString的變化,來更新視圖控制器的頭像UIImageView
    1. 視圖控制器對 viewModel 起如下作用:
    • 每當 UITextField 中的文本發生變化, 更新 viewModel上的 readwrite屬性 mobilePhone或者verifyCode
    • 登錄按鈕被點擊時,調用viewModel上的loginSuccess:failure方法。
    1. 視圖控制器不要做的事
    • 發起登錄的網絡請求
    • 判定登錄按鈕的有效性
    • 來獲取頭像的地址(PS:有可能從本地數據庫獲取,也有可能通過網絡請求來獲取)
    • ...

    請再次注意視圖控制器總的責任是處理viewModel中的變化。

五、MVVM Without ReactiveCocoa的商品首頁界面的實踐
  • ViewModel的設計
/// 商品首頁的視圖模型 -- 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;
/**
 * 加載網絡數據 通過block回調減輕view 對 viewModel 的狀態的監聽
 @param success 成功的回調
 @param failure 失敗的回調
 @param configFooter 底部刷新控件的狀態 lastPage = YES ,底部刷新控件hidden,反之,show
 */
- (void)loadData:(void(^)(id json))success
         failure:(void(^)(NSError *error))failure
    configFooter:(void(^)(BOOL isLastPage))configFooter;
@end
  • ViewController(視圖控制器)

    視圖控制器通過調用viewModelloadBannerData:failure:loadData:failure:configFooter:來獲取商品首頁的廣告數據(SUBanner)以及商品數據(SUGoods)視圖控制器通過使用viewModel上的bannersdataSource數組中的對象來配置表格視圖(tableView)的tableViewHeadercell。通常我們會期待展現 dataSource 的是數據-模型對象。同時你可能已經對其感到奇怪, 因為我們試圖通過 MVVM模式不暴漏數據-模型對象。 (前面提到過的)。
    假設我們暴露數據-模型(SUGoods),那就分析如下:

商品首頁暴露數據模型.png

我們不瞎,明顯從上圖??可以看出視圖 SUGoodsCell直接引用了模型SUGoods,這就有悖了MVVM的初衷:** view和 view controller 都不能直接引用model,而是引用視圖模型(viewModel) **

  • 子ViewModel

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

商品首頁子視圖.png

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

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

商品的用戶信息.png

商品模型(SUGoods)的數據結構如下:

/** 商品運費類型 */
typedef NS_ENUM(NSUInteger, SUGoodsExpressType) {
    SUGoodsExpressTypeFree = 0,   // 包郵
    SUGoodsExpressTypeValue = 1,  // 運費
    SUGoodsExpressTypeFeeding = 2,// 待議
};
@interface SUGoods : SUModel
/// === 商品相關的屬性 ===
....
/// === 商品中的用戶相關的信息 ===
/// 用戶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

假設我們將數據-模型通過屬性暴露在子視圖模型的.h中,筆者將設計SUGoodsItemViewModel.h/m大致代碼如下??:

/// SUGoodsItemViewModel.h
/// 數據-模型(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;
}

筆者將設計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;
 }

假設我們將數據-模型不通過屬性暴露在子視圖模型的.h中,筆者將設計SUGoodsItemViewModel.h/m大致代碼如下??:

/// SUGoodsItemViewModel.h
/// 數據-模型(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跳轉到用戶信息的界面
@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;
}

筆者將設計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;
 }

首先我們發現,如果不通過屬性暴露數據模型,SUGoodsItemViewModelSUGoods也太想了吧,僅僅只是用readonly代替readwirte而已!為啥吃飽了事沒飯干將其轉化成 viewModel 的工作啊?神經病啊!!即使類似,viewModel 讓我們限制信息只暴露給我們需要的地方, 提供額外數據轉換的屬性, 或為特定的視圖計算數據。(此外,當可以不暴露可變數據-模型對象(SUGoods)時也是極好的,因為我們希望 viewModel 自己承擔起更新它們的任務,而不是靠視圖或視圖控制器。)
但是日常開發過程中筆者 強烈建議大家把數據模型(SUGoods)暴露在子視圖模型(SUGoodsItemViewModel)的.h中。這樣一來子視圖模型的屬性會相應的減少,大大減少了膠水代碼的產生。但是可能又會有人不想說話并向筆者拋了一個issue!!!
既然通過屬性暴露了數據-模型(SUGoods)了,為何還要暴露一個userId的屬性?有必要嗎?很有必要!!!
上面已經提到過ViewModel 提供額外數據轉換的屬性, 或為特定的視圖計算數據。顯然我們完全可以不暴露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] ;
 }

對此,筆者只能微微一笑很傾城了。因為這個數據的屬性過于簡單,僅僅只是數據的拼接,看不出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中編寫這段又臭又長的邏輯代碼。

六、劃重點,漲姿勢
  • 保證將MVVMModel設計成Thin-Model(瘦模型),避免其淪為Fat-Model(胖模型),且不要與ViewModel混淆一談,兩者道不同,不相為謀
  • ViewViewModel之間存在數據和事件的雙向綁定的關系,利用 ReactiveCocoa 來充當viewviewModel 之間 binder 的角色,優雅地實現兩者之間的數據綁定(同步),切記:ReactiveCocoa 并非是實現MVVM設計模式的充要條件。MVVM的關鍵是要有ViewModel!而不是 ReactiveCocoa
  • MVVM可以看成是MVMCV的設計模式,從而引申出來ModelViewModelController以及View他們之間的角色定位,以及各自的職責所在。切勿試圖萌生用ViewModel來代替ViewControllerControllerMVVM中負責ViewViewModel之間的綁定和常規的UI邏輯處理,而ViewModel目的在于抽離ViewController中展示業務邏輯。ViewModelViewController在一起,但獨立。
  • view/viewController 中不能直接引用模型ModelviewModel 不必在屏幕上顯示所有東西。針對實際的業務,將一組業務邏輯相關的代碼抽取到一個獨立的視圖模型中處理(子ViewModel)。
  • 視圖模型可以通過屬性的方式暴露一個只讀數據模型,視圖模型負責提供額外數據轉換的屬性, 或為特定的視圖提供計算數據。為了消除View過多的觀察ViewModel的狀態(屬性)的變化,我們可以通過block的方式回調請求數據。
七、代碼閱讀

由于這個功能筆者分別采用 MVCMVVM Without ReactiveCococa來開發實踐,畢竟蘿卜白菜,各有所愛,目的就是便于大家更深層次的了解MVCMVVM的異同,以及提供一個利用MVVM Without ReactiveCococa真實開發的樣例,希望能夠打消大家對 MVVM 模式的顧慮。為了方便我們從宏觀上了解功能的的整體結構,我們可以分別看看MVCMVVM Without RAC的類圖。大家可以跟著類圖,順藤摸瓜,秉承該看的看,不該看的偷偷看的原則,趕快行動起來吧。

  • MVC類圖


    MVC類圖.png
八、期待
  1. 文章若對您有點幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學無止境。
  2. 針對文章所述內容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。
  3. GitHub地址:https://github.com/CoderMikeHe
九、參考鏈接
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容

  • 一、概述 筆者 強烈推薦 大家在閱讀本文之前,還請先移步閱讀?? iOS 關于MVC和MVVM設計模式的那些事 和...
    CoderMikeHe閱讀 15,630評論 28 147
  • 一、概述 在 iOS 開發中,MVC(Model View Controller)是構建iOS App的標準模式,...
    CoderMikeHe閱讀 26,897評論 70 347
  • 傳統模式下的開發MVCMVVM基于面向協議MVP的介紹MVP實戰開發說在前面:相信就算你是個iOS新手也應該聽說過...
    行走的菜譜閱讀 3,172評論 1 5
  • 白雪皚皚, 蒼松陣陣, 人跡罕至, 笨重的黑熊為了食物在雪地覓食 看到了陽光下 雪地里 那一簇神奇的黃色 那樣柔嫩...
    海深深閱讀 482評論 0 0
  • 亨利·羅林斯說:“生命中一半是糟心的事,一半是如何解決這些糟心的事” 我說:“生命中沒有糟心的事,糟心的事都是自找...
    1860fb3b42da閱讀 230評論 0 1