聊聊iOS開(kāi)發(fā)之MVVM的架構(gòu)設(shè)計(jì)

前言

在開(kāi)發(fā)App的時(shí)候,我們的基本目標(biāo)一般有以下幾點(diǎn):

- `可靠性 - App的功能能夠正常使用`
- `健壯性 - 在用戶非正常使用的時(shí)候,app也能夠正常反應(yīng),不要崩潰`
- `效率性 - 啟動(dòng)時(shí)間,耗電歧譬,流量,界面反應(yīng)速度在用戶容忍的范圍以內(nèi)`

上面三點(diǎn)是表象層的東西搏存,是大多數(shù)開(kāi)發(fā)者或者團(tuán)隊(duì)會(huì)著重注意的。除了這三點(diǎn)矢洲,還有一些目標(biāo)是工程方面的也是開(kāi)發(fā)者要注意的:

- `可修改性/可擴(kuò)展性 - 軟件需要迭代璧眠,功能不斷完善`
- `容易理解 - 代碼能夠容易理解`
- `可測(cè)試性 - 代碼能夠方便的編寫(xiě)單元測(cè)試和集成測(cè)試`
- `可復(fù)用性 - 不用一次又一次造輪子`

基于這些設(shè)計(jì)目標(biāo)和理念,軟件設(shè)計(jì)領(lǐng)域又有了設(shè)計(jì)模式读虏。MVC/MVVM都是就是設(shè)計(jì)模式的一種责静。

在MVC的架構(gòu)中,Model持有數(shù)據(jù)盖桥,View顯示與用戶交互的界面灾螃,而ViewController調(diào)解Model和View之間的交互。  
現(xiàn)在揩徊,MVC 依然是目前主流客戶端編程框架腰鬼,但同時(shí)它也被調(diào)侃成Massive View Controller(重量級(jí)視圖控制器),
開(kāi)發(fā)者在開(kāi)發(fā)中無(wú)可避免被下面幾個(gè)問(wèn)題所困擾:

- 厚重的ViewController
- 遺失的網(wǎng)絡(luò)邏輯(無(wú)立足之地)
- 較差的可測(cè)試性

而MVVM這種新的代碼組織方式就可以解決這些問(wèn)題塑荒,本文就MVVM的架構(gòu)設(shè)計(jì)做個(gè)簡(jiǎn)單的個(gè)人總結(jié)熄赡。

MVVM概述

 從圖中我們可以看到MVVM的關(guān)系基本是:View <-> C <-> ViewModel <-> Model,

嚴(yán)格來(lái)說(shuō)MVVM其實(shí)是MVCVM齿税。Controller夾在View和ViewModel之間做的其中一個(gè)主要事情就是將View和ViewModel進(jìn)行綁定. 在邏輯上彼硫,Controller知道應(yīng)當(dāng)展示哪個(gè)View,Controller也知道應(yīng)當(dāng)使用哪個(gè)ViewModel凌箕, 然而View和ViewModel它們之間是互相不知道的拧篮,所以Controller就負(fù)責(zé)控制他們的綁定關(guān)系。

MVVM 一種可以很好地解決Massive View Controller問(wèn)題的辦法
就是將 Controller 中的展示邏輯抽取出來(lái)牵舱,放置到一個(gè)專(zhuān)門(mén)的地方串绩,
而這個(gè)地方就是 viewModel 。MVVM衍生于MVC仆葡,是對(duì) MVC 的一種演進(jìn)赏参,
它促進(jìn)了 UI 代碼與業(yè)務(wù)邏輯的分離志笼。
它正式規(guī)范了視圖和控制器緊耦合的性質(zhì),并引入新的組件把篓。他們之間的結(jié)構(gòu)關(guān)系如下:

不難看出纫溃,MVVM是對(duì)MVC的擴(kuò)展,所以MVVM可以完美的兼容MVC韧掩。
對(duì)于一個(gè)界面來(lái)說(shuō)紊浩,有時(shí)候View和ViewModel往往不止一個(gè),MVVM也可以組合使用:

MVVM 的基本概念

- 在MVVM 中疗锐,view 和 view controller正式聯(lián)系在一起坊谁,我們把它們視為一個(gè)組件,
  Controller可以當(dāng)作一個(gè)重量級(jí)的View(負(fù)責(zé)界面切換和處理各類(lèi)系統(tǒng)事件)滑臊。
- view 和 view controller 都不能直接引用model口芍,而是引用視圖模型(viewModel)
- viewModel 是一個(gè)放置用戶輸入驗(yàn)證邏輯,視圖顯示邏輯雇卷,發(fā)起網(wǎng)絡(luò)請(qǐng)求和其他代碼的地方, 
  它的職責(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)行綁定贮折。
- 使用MVVM會(huì)輕微的增加代碼量裤翩,但總體上減少了代碼的復(fù)雜性。

MVVM 的注意事項(xiàng)

- viewController 盡量不涉及業(yè)務(wù)邏輯调榄,讓 viewModel 去做這些事情踊赠。
- viewController 只是一個(gè)中間人,接收 view 的事件振峻、調(diào)用  viewModel 的方法臼疫、響應(yīng) viewModel 的變化。
  一方面負(fù)責(zé)View和ViewModel之間的綁定扣孟,另一方面也負(fù)責(zé)常規(guī)的UI邏輯處理烫堤。
- view 引用viewModel ,但反過(guò)來(lái)不行(即不要在viewModel中引入#import UIKit.h凤价,
  任何視圖本身的引用都不應(yīng)該放在viewModel中)(PS:基本要求鸽斟,必須滿足)
- viewModel 引用model,但反過(guò)來(lái)不行
- viewModel 絕對(duì)不能包含視圖 view(UIKit.h)利诺,不然就跟 view 產(chǎn)生了耦合富蓄,不方便復(fù)用和測(cè)試。
- viewModel之間可以有依賴(lài)慢逾。
- viewModel避免過(guò)于臃腫立倍,否則重蹈Controller的覆轍灭红,變得難以維護(hù)。

關(guān)于MVVM Without ReactiveCocoa

為了讓View和ViewModel之間能夠有比較松散的綁定關(guān)系口注,于是我們使用ReactiveCocoa变擒,
KVO,Notification寝志,block娇斑,delegate和target-action都可以用來(lái)做數(shù)據(jù)通信,
從而來(lái)實(shí)現(xiàn)綁定材部,但都不如ReactiveCocoa提供的RACSignal來(lái)的優(yōu)雅毫缆,
 使用函數(shù)響應(yīng)式框架能更好的實(shí)現(xiàn)數(shù)據(jù)和視圖的雙向綁定(ViewModel的數(shù)據(jù)可以顯示到View上,
View上的操作同樣會(huì)引起ViewModel的變化),降低了ViewModel和View的耦合度乐导。 
如果不用ReactiveCocoa苦丁,綁定關(guān)系可能就做不到那么松散那么好,但并不影響它還是MVVM物臂。

MVVM的關(guān)鍵是要有ViewModel芬骄。而不是ReactiveCocoa、RXSwift或RXJava等鹦聪。
而在現(xiàn)實(shí)中我傾向于使用 block而不是 KVO,因?yàn)镵VO的代碼量太大了蒂秘,block則簡(jiǎn)潔的多泽本。

ReactiveCocoa或RXSwift通過(guò)這兩個(gè)框架可以實(shí)現(xiàn)ViewModel和View的雙向綁定,
但同樣會(huì)存在幾個(gè)比較重大的問(wèn)題姻僧。 首先,ReactiveCocoa或RXSwift的學(xué)習(xí)成本很高规丽;
其次,

數(shù)據(jù)綁定使得 Bug 很難被調(diào)試撇贺,當(dāng)界面出現(xiàn)異常赌莺,可能是View的問(wèn)題,也可能是數(shù)據(jù)ViewModel的問(wèn)題松嘶。 而數(shù)據(jù)綁定會(huì)使一個(gè)位置的bug傳遞到其他位置艘狭,難以定位。

MVVM Without ReactiveCocoa的一個(gè)應(yīng)用實(shí)例

下面的內(nèi)容源自這篇文章翠订,我覺(jué)得舉例很得到就引用過(guò)來(lái)了:原文在這里

  • 效果圖


  • 登錄頁(yè)面邏輯分析圖


  • 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目代、errorresponseObject的屬性即可嗤练,代碼大致如下:

_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)操作煞抬,我們就要編寫(xiě)這么多代碼霜大,試想如果再多一個(gè)操作呢?再多兩個(gè)操作呢革答?.... 如果不用block回調(diào)战坤,不管你們會(huì)不會(huì),總之残拐,我會(huì)途茫。下面??再看看利用block的回調(diào)實(shí)現(xiàn),你們就會(huì)解惑溪食,釋?xiě)蚜四也罚鸫a好受點(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玉掸。

2、視圖控制器對(duì) viewModel 起如下作用:

每當(dāng) UITextField 中的文本發(fā)生變化, 更新 viewModel上的 readwrite屬性 mobilePhone或者verifyCode
登錄按鈕被點(diǎn)擊時(shí)醒叁,調(diào)用viewModel上的loginSuccess:failure方法司浪。

3、視圖控制器不要做的事

發(fā)起登錄的網(wǎng)絡(luò)請(qǐng)求
判定登錄按鈕的有效性
來(lái)獲取頭像的地址(PS:有可能從本地?cái)?shù)據(jù)庫(kù)獲取辐益,也有可能通過(guò)網(wǎng)絡(luò)請(qǐng)求來(lái)獲榷习痢)
...

請(qǐng)?jiān)俅巫⒁庖晥D控制器總的責(zé)任是處理viewModel中的變化。

商品首頁(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),那就分析如下:

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

從上面??可知包雀,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ù):

商品模型(SUGoods)的數(shù)據(jù)結(jié)構(gòu)如下:

/** 商品運(yùn)費(fèi)類(lè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;
/// 用戶昵稱(chēng):
@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];
      /// 昵稱(chēng)
      self.userNameLabel.text = viewModel.goods.nickName;
     /// 芝麻認(rèn)證
      self.realNameIcon.hidden = !viewModel.goods.iszm;
      /// 用戶ID
      self.userIdLabel.text = viewModel.userId;
 }

既然通過(guò)屬性暴露了數(shù)據(jù)-模型(SUGoods)了吆鹤,為何還要暴露一個(gè)userId的屬性?有必要嗎洲守?很有必要R晌瘛!梗醇!
上面已經(jīng)提到過(guò)ViewModel 提供額外數(shù)據(jù)轉(zhuǎn)換的屬性, 或?yàn)樘囟ǖ囊晥D計(jì)算數(shù)據(jù)知允。顯然我們完全可以不暴露userId,僅僅只要我們?cè)赟UGoodsCell.m中這樣寫(xiě)即可婴削,根本無(wú)傷大雅是吧廊镜。

  ///  SUGoodsCell.m
 - (void)bindViewModel:(SUGoodsItemViewModel *)viewModel
{
      self.viewModel = viewModel;
      /// 頭像
      [MHWebImageTool setImageWithURL:viewModel.goods.avatar placeholderImage:placeholderUserIcon() imageView:self.userHeadImageView];
      /// 昵稱(chēng)
      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ì)把上面??這段代碼寫(xiě)在ViewModel中,通過(guò)暴露一個(gè)只讀(readonly)的freightExplain屬性供cell獲取展示衡楞,而不是Cell中編寫(xiě)這段又臭又長(zhǎng)的邏輯代碼吱雏。

基于 MVVM 的更瘦身的架構(gòu)設(shè)計(jì)方式

MVVM的出現(xiàn)主要是為了解決在開(kāi)發(fā)過(guò)程中Controller越來(lái)越龐大的問(wèn)題,變得難以維護(hù)瘾境,
所以MVVM把數(shù)據(jù)加工的任務(wù)從Controller中解放了出來(lái)歧杏,使得Controller只需要專(zhuān)注于數(shù)據(jù)調(diào)配的工作,  
ViewModel則去負(fù)責(zé)數(shù)據(jù)加工并通過(guò)通知機(jī)制讓View響應(yīng)ViewModel的改變迷守。

MVVM是基于胖Model的架構(gòu)思路建立的犬绒,然后在胖Model中拆出兩部分:Model和ViewModel。
ViewModel本質(zhì)上算是Model層(因?yàn)槭桥諱odel里面分出來(lái)的一部分)兑凿,所以 ViewModel里面不能包含任何 UIKit的內(nèi)容凯力。
而且View并不一定適合直接持有ViewModel,因?yàn)閂iewModel有可能并不是只服務(wù)于特定的一個(gè)View礼华,
如果我們對(duì)于單個(gè)復(fù)雜View設(shè)計(jì)一個(gè) ViewModel 是可以讓該 View 持有該 ViewModel的咐鹤。

如圖我們?cè)O(shè)計(jì)了一個(gè)基于 MVVM 的更瘦身的架構(gòu)設(shè),這個(gè)架構(gòu)中:


*   View - 用來(lái)呈現(xiàn)用戶界面
*   ViewManger - 用來(lái)處理View的常規(guī)事件圣絮,負(fù)責(zé)管理View
*   Controller - 負(fù)責(zé)ViewManger和ViewModel之間的綁定祈惶,負(fù)責(zé)控制器本身的生命周期。
*   ViewModel - 存放各種業(yè)務(wù)邏輯和網(wǎng)絡(luò)請(qǐng)求,不能存在 UIKit 有關(guān)的東西行瑞。
*   Model - 用來(lái)呈現(xiàn)數(shù)據(jù)

這種設(shè)計(jì)的目的是保持View和Model的高度純潔奸腺,提高可擴(kuò)展性和復(fù)用度。
在日常開(kāi)發(fā)中血久,ViewModel是為了拆分Controller業(yè)務(wù)邏輯而存在的突照,
所以ViewModel需要提供公共的服務(wù)接口,以便為Controller提供數(shù)據(jù)氧吐。
而ViewManger的作用相當(dāng)于一個(gè)小管家讹蘑,幫助Controller來(lái)分別管理每個(gè)subView,ViewManger負(fù)責(zé)接管來(lái)自View的事件筑舅,
也負(fù)責(zé)接收來(lái)自Controller的模型數(shù)據(jù)座慰,
而View進(jìn)行自己所負(fù)責(zé)的視圖數(shù)據(jù)綁定工作。
Controller則是最后的大家長(zhǎng)翠拣,負(fù)責(zé)將ViewModel和ViewManger進(jìn)行綁定版仔,
進(jìn)行數(shù)據(jù)轉(zhuǎn)發(fā)工作。把合適的數(shù)據(jù)模型分發(fā)給合適的視圖管理者误墓。

這樣的架構(gòu)設(shè)計(jì)蛮粮,就像一條生產(chǎn)線,ViewModel進(jìn)行數(shù)據(jù)的采集和加工谜慌,Controller則進(jìn)行數(shù)據(jù)的裝配和轉(zhuǎn)發(fā)工作然想,ViewManger進(jìn)行接收轉(zhuǎn)發(fā)分配來(lái)的數(shù)據(jù),從而進(jìn)行負(fù)責(zé)View的展示工作和管理View的事件欣范。這樣变泄,不管哪個(gè)環(huán)節(jié),都是可以更換的恼琼,同時(shí)也提高了復(fù)用性妨蛹。

總結(jié)

iOS App是一個(gè)麻雀雖小,五臟俱全的軟件晴竞。良好的架構(gòu)和設(shè)計(jì)能夠讓代碼容易理解和維護(hù)滑燃,并且不易出錯(cuò)。但是本文可能也存在錯(cuò)誤之處颓鲜,或者不足之處,希望大家看到有問(wèn)題的地方在下方留言一起談?wù)搶W(xué)習(xí)典予,后續(xù)可能會(huì)持續(xù)更新更正本文甜滨。

參考文章:
https://github.com/lovemo/MVVMFramework/tree/master/source
MVVM與Controller瘦身實(shí)踐
iOS 關(guān)于MVC和MVVM設(shè)計(jì)模式的那些事
iOS 關(guān)于MVVM Without ReactiveCocoa設(shè)計(jì)模式的那些事

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瘤袖,隨后出現(xiàn)的幾起案子衣摩,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艾扮,死亡現(xiàn)場(chǎng)離奇詭異既琴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)泡嘴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)甫恩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人酌予,你說(shuō)我怎么就攤上這事磺箕。” “怎么了抛虫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵松靡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我建椰,道長(zhǎng)雕欺,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任棉姐,我火速辦了婚禮屠列,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谅海。我一直安慰自己脸哀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布扭吁。 她就那樣靜靜地躺著撞蜂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪侥袜。 梳的紋絲不亂的頭發(fā)上蝌诡,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音枫吧,去河邊找鬼浦旱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛九杂,可吹牛的內(nèi)容都是我干的颁湖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼例隆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼甥捺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起镀层,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤镰禾,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體吴侦,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屋休,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了备韧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片劫樟。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖盯蝴,靈堂內(nèi)的尸體忽然破棺而出毅哗,到底是詐尸還是另有隱情,我是刑警寧澤捧挺,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布虑绵,位于F島的核電站,受9級(jí)特大地震影響闽烙,放射性物質(zhì)發(fā)生泄漏翅睛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一黑竞、第九天 我趴在偏房一處隱蔽的房頂上張望捕发。 院中可真熱鬧,春花似錦很魂、人聲如沸扎酷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)法挨。三九已至,卻和暖如春幅聘,著一層夾襖步出監(jiān)牢的瞬間凡纳,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工帝蒿, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荐糜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓葛超,卻偏偏與公主長(zhǎng)得像暴氏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绣张,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容