一、概述
- 通過上一篇文章的學習,我們對關于
MVC
的弊端的產生和MVVM
中viewModel
的職責及其使用注意事項,想必都有了些許了解和認識,最起碼What is MVC ? What is MVVM ?
,大家也不會感覺這是最熟悉的陌生人了吧。筆者不才,本文將著重談談MVVM
在iOS開發中的實際運用,以及自身通過實踐探索出來的經驗之談,同時希望能讓大家更加深刻體會到MVVM
中M
、V
、VM
各自的職責,以及V
和VM
之間那份剪不斷,理還亂的纏綿往事。 - 本文只是筆者在實踐
MVVM
過程中的些許見解,在此拋磚引玉,共同探討下MVVM
的實踐思路,希望能夠打消你對MVVM
模式的顧慮 ,提供一點思路,少走一些彎路,填補一些細坑。文章僅供大家參考,若有不妥之處,還望不吝賜教,歡迎批評指正。 -
MVVM
基礎知識以及其使用注意不了解的,請務必戳我?? iOS 關于MVC和MVVM設計模式的那些事
二、MVVM
- MVVM的基本概念
-
MVVM的結構圖
MVVM結構圖.png - MVVM的定義
從上圖中,我們可以非常清楚地看到 MVVM 中四個組件之間的關系。注:除了view
、viewModel
和model
之外,MVVM
中還有一個非常重要的隱含組件binder
:
Model :和MVC
中的model
保持一致,完全取決于你的"偏好設置"。你可能會為model
封裝一些額外的操作數據的業務邏輯,雖然蘋果是推崇你這么干的,但是筆者認為不妥,這樣很可能會導致一個胖Model
的產生,而且胖Model相對比較難移植
,胖Model隨著產品的迭代會更加的Fat
,最終難以維護,一胖毀所有
。我更傾向于把它當做一個容納表現數據-模型(data-model
)對象信息的結構體(瘦Model
),并通過一個單獨的管理類來維護/創建/管理模型的統一邏輯,又或者可以通過使用Category
來擴充業務邏輯。MVVM是基于胖Model
的架構思路建立的,然后在胖Model
中拆出兩部分:Model
和ViewModel
(PS:感覺是否有點道理)。
View:由MVC
中的view
和controller
組成,負責 UI 的展示,綁定viewModel
中的屬性,觸發viewModel
中的命令以及呈現由viewModel
提供的數據。
View-Model: 千萬不要把它與傳統數據-模型結構中模型混為一談。 它的職責之一就是作為一個表現視圖顯示自身所需數據的靜態模型;但它也有收集, 解釋和轉換那些數據的責任。它是從MVC
的controller
中抽取出來的展示邏輯,負責從model
中獲取view
所需的數據,轉換成view
可以展示的數據,并暴露公開的屬性和命令供view
進行綁定。
Binder:在MVVM
中,聲明式的數據和命令綁定是一個隱含的約定,它可以讓開發者非常方便地實現view
和viewModel
的同步,避免編寫大量繁雜的樣板化代碼。在MVVM
實現中,利用 ReactiveCocoa 來在view
和viewModel
之間充當binder
的角色,優雅地實現兩者之間的數據綁定(同步)。
- MVVM與MVC聯系
-
職責劃分
MVVM若按照職責來劃分的話,其根據首字母縮寫如同view-model
術語一樣, 對如何使用它們進行 iOS 開發體現得有點不太準確。
根據MVC
和MVVM
的職責劃分,我們利用圖解來表示,首先我們顛倒了MVC
中的V
和C
,于是首字母縮寫更能準確地反映出實際開發中組件間的關系方位,給我們帶來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
不談,突然發現這樣的ViewModel
、Mode
以及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`的正確打開方式如下:

從上圖可知,`Controller`夾在`View`和`ViewModel`之間做的其中一個主要事情就是將`View`和`ViewModel`進行綁定。在邏輯上,`Controller`知道應當展示哪個`View`,`Controller`也知道應當使用哪個`ViewModel`來提供數據,然而`View`和`ViewModel`它們之間是互相不知道的,所以Controller僅關注于用 `view-model 的數據配置`和`管理各種各樣的視圖`。
所以Controller
在MVVM
中,一方面負責View
和ViewModel
之間的綁定,另一方面也負責常規的UI邏輯處理
。(PS:豁然開朗了沒?柳暗花明了沒?Six Six Six...)
-
MVVM模塊層級圖
模塊層級圖.png
三、MVVM Without ReactiveCocoa功能實踐的前期準備
Talk is cheap,Show me the code。光說不練假把式,光練不說啥把式
。使用 MVVM
搭配 ReactiveCocoa
會很優雅地實現View
和ViewModel
之間的數據綁定,不過它的問題在于學習成本和維護成本比較高,但是切記: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
|
- 需求分析表
用戶登錄需求 | 商品首頁需求 |
---|---|
只有用戶輸入了手機號和驗證碼,登錄按鈕才可點擊 | 界面滾動流暢,縱享絲滑 |
用戶輸入的手機號必須是真實有效的 | 導航欄的樣式根據用戶的滾動而變化 |
驗證碼為四位有效數字 | 點擊右下角的卡通頭像,滾動頂部 |
當用戶輸入手機號碼時需要從本地獲取用戶頭像 | 響應商品界面上的事件處理,如商品、用戶頭像、地理位置、留言和點贊的事件處理 |
備注:右上角的填充 按鈕,僅僅是減少開發者的輸入(筆者的需求) |
備注:點擊頂部搜索框,回退到列表頁(筆者的需求) |
- 效果圖
四、MVVM Without ReactiveCocoa的登錄界面的實踐
- 邏輯分析圖
- 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
的方法監聽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);
/// 根據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(視圖控制器)
- 視圖控制器從
viewModel
獲取的數據將用來:
- 當
validLogin
的值發生變化時,觸發登錄按鈕
的enabled
的屬性。 - 監聽
avatarUrlString
的變化,來更新視圖控制器的頭像
的UIImageView
。
- 視圖控制器對
viewModel
起如下作用:
- 每當
UITextField
中的文本發生變化, 更新viewModel
上的readwrite
屬性mobilePhone
或者verifyCode
- 登錄按鈕被點擊時,調用
viewModel
上的loginSuccess:failure
方法。
- 視圖控制器不要做的事
- 發起登錄的網絡請求
- 判定登錄按鈕的有效性
- 來獲取頭像的地址(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(視圖控制器)
視圖控制器
通過調用viewModel
的loadBannerData:failure:
和loadData:failure:configFooter:
來獲取商品首頁的廣告數據(SUBanner)
以及商品數據(SUGoods)
。視圖控制器
通過使用viewModel
上的banners
和dataSource
數組中的對象來配置表格視圖(tableView
)的tableViewHeader
和cell
。通常我們會期待展現dataSource
的是數據-模型
對象。同時你可能已經對其感到奇怪, 因為我們試圖通過MVVM
模式不暴漏數據-模型
對象。 (前面提到過的)。
假設我們暴露數據-模型(SUGoods)
,那就分析如下:
我們不瞎,明顯從上圖??可以看出視圖 SUGoodsCell
直接引用了模型SUGoods
,這就有悖了MVVM
的初衷:** view和 view controller 都不能直接引用model,而是引用視圖模型(viewModel) **
-
子ViewModel
我們必須明確:viewModel不必在屏幕上顯示所有東西。在工作中如果遇到量級非常重的控制器,可以針對實際的業務,將一組業務邏輯相關的代碼抽取到一個獨立的視圖模型中處理。你可用
子viewModel
來代表屏幕上更小的、更潛在的被封裝的部分。
一般來說,viewController
可以帶一個viewModel
,那如果出現Cell
時怎么辦,Cell
里又包含了按鈕,按鈕又需要數據請求又怎么處理?這些都是比較常見的場景,也可以通過MVVM
來解決。
我們知道viewModel
的職責是為view
提供數據支持,Cell
也是一個View
,那么為Cell
配備一個viewModel
不就可以了么。所以相對于ViewController
的ViewModel
來說,Cell
上配備的viewModel
就是子viewModel
。
你不總是需要子viewModel
。 比如,筆者可能用表格tableHeaderView
視圖來渲染簡單的頁面展示。它不是個可重用的組件,所以筆者可能僅將我們已經給視圖控制器用過的相同的viewModel
傳給那個自定義的header
視圖。它會用到viewModel
中它需要的信息,而無視余下的部分。
針對上面??發現的問題,筆者優化如下:
從上面??可知,
dataSource
是一個里面裝著SUGoodsItemViewModel
的對象數組,在表格視圖中的 tableView: cellForRowAtIndexPath:
方法中,將會從視圖控制器的viewModel
的dataSource
中通過正確的索引獲取到子viewModel
, 并把它賦值給 cell
上的 viewModel
屬性。
想必大家還有一個疑惑,數據-模型(SUGoods)是否要通過屬性的方式暴露在子視圖模型(SUGoodsItemViewModel)的.h文件中?
我們假設要通過SUGoodsItemViewModel
來提供給SUGoodsCell
展示下面??的界面的數據:
商品模型(
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;
}
首先我們發現,如果不通過屬性暴露數據模型,SUGoodsItemViewModel
跟SUGoods
也太想了吧,僅僅只是用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
中編寫這段又臭又長的邏輯代碼。
六、劃重點,漲姿勢
- 保證將
MVVM
中Model
設計成Thin-Model(瘦模型)
,避免其淪為Fat-Model(胖模型)
,且不要與ViewModel
混淆一談,兩者道不同,不相為謀
。 -
View
和ViewModel
之間存在數據和事件的雙向綁定的關系,利用 ReactiveCocoa 來充當view
和viewModel
之間binder
的角色,優雅地實現兩者之間的數據綁定(同步),切記:ReactiveCocoa 并非是實現MVVM
設計模式的充要條件。MVVM
的關鍵是要有ViewModel
!而不是 ReactiveCocoa -
MVVM
可以看成是MVMCV
的設計模式,從而引申出來Model
、ViewModel
、Controller
以及View
他們之間的角色定位,以及各自的職責所在。切勿試圖萌生用ViewModel
來代替ViewController
,Controller
在MVVM
中負責View
和ViewModel
之間的綁定和常規的UI邏輯處理
,而ViewModel
目的在于抽離ViewController
中展示業務邏輯。ViewModel
和ViewController
在一起,但獨立。 - 在
view/viewController
中不能直接引用模型Model
,viewModel
不必在屏幕上顯示所有東西。針對實際的業務,將一組業務邏輯相關的代碼抽取到一個獨立的視圖模型中處理(子ViewModel
)。 -
視圖模型
可以通過屬性的方式暴露一個只讀
的數據模型
,視圖模型負責提供額外數據轉換的屬性, 或為特定的視圖提供計算數據。為了消除View
過多的觀察ViewModel
的狀態(屬性)的變化,我們可以通過block
的方式回調請求數據。
七、代碼閱讀
由于這個功能筆者分別采用 MVC
和MVVM Without ReactiveCococa
來開發實踐,畢竟蘿卜白菜,各有所愛,目的就是便于大家更深層次的了解MVC
和MVVM
的異同,以及提供一個利用MVVM Without ReactiveCococa
真實開發的樣例,希望能夠打消大家對 MVVM
模式的顧慮。為了方便我們從宏觀上了解功能的的整體結構,我們可以分別看看MVC
和MVVM Without RAC
的類圖。大家可以跟著類圖,順藤摸瓜,秉承該看的看,不該看的偷偷看的原則,趕快行動起來吧。
-
MVC類圖
MVC類圖.png
-
MVVM Without RAC 類圖
MVVMWithoutRAC類圖.png 源碼地址(PS: 還請star一下,不會懷孕??的)
MHDevelopExample_Objective_C 目錄中的 MVC&MVVM文件夾中
八、期待
- 文章若對您有點幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學無止境。
- 針對文章所述內容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。
- 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