前言
在開(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
目代、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ě)蚜四也罚鸫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)用viewModel
的loadBannerData:failure:
和loadData:failure:configFooter:
來(lái)獲取商品首頁(yè)的廣告數(shù)據(jù)(SUBanner)
以及商品數(shù)據(jù)(SUGoods)
续捂。視圖控制器
通過(guò)使用viewModel
上的banners
和dataSource
數(shù)組中的對(duì)象來(lái)配置表格視圖(tableView
)的tableViewHeader
和cell
垦垂。通常我們會(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ì)于ViewController
的ViewModel
來(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ì)從視圖控制器的viewModel
的dataSource
中通過(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ì)模式的那些事