聊聊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è)專門的地方,
而這個(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é)界面切換和處理各類系統(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之間可以有依賴。
- 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、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)操作,我們就要編寫(xiě)這么多代碼,試想如果再多一個(gè)操作呢?再多兩個(gè)操作呢?.... 如果不用block回調(diào),不管你們會(huì)不會(huì),總之,我會(huì)。下面??再看看利用block的回調(diào)實(shí)現(xiàn),你們就會(huì)解惑,釋?xiě)蚜?,起碼好受點(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)類型 */
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;
 }

既然通過(guò)屬性暴露了數(shù)據(jù)-模型(SUGoods)了,為何還要暴露一個(gè)userId的屬性?有必要嗎?很有必要?。?!
上面已經(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];
      /// 昵稱
      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只需要專注于數(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)系作者
【社區(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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