為什么要用MVVM替代MVC
Apple倡導(dǎo)開發(fā)者們使用MVC模式開發(fā)App程序,但很多人都沒(méi)有嚴(yán)格按照MVC的模式去開發(fā),只是讓程序的架構(gòu)看上去像MVC,而實(shí)際上是MC或VC。
很多入門開發(fā)者都有一個(gè)通病,就是把所有的邏輯,界面生成都寫進(jìn)ViewController中,這樣ViewController就變成了一個(gè)Massive View Controller(重量級(jí)視圖控制器)。重量級(jí)視圖控制器會(huì)讓整個(gè)ViewController變得非常復(fù)雜且不可維護(hù),讓維護(hù)者崩潰卻無(wú)從下手,只能忍痛默默的重寫整個(gè)邏輯。
但是,有一種解決方案,可以解決Massive View Controller的問(wèn)題,那就是MVVM。
這是傳統(tǒng)MVC模式:

這是MVVM模式:

很多時(shí)候新手們會(huì)把數(shù)據(jù)轉(zhuǎn)換邏輯,網(wǎng)絡(luò)請(qǐng)求邏輯等都放到ViewController中,這樣會(huì)不可避免的讓ViewController變得臃腫,這是造成重量級(jí)視圖控制器的重要原因。
除了上面提到的一個(gè)原因外,由于AFNetworking是iOS開發(fā)網(wǎng)絡(luò)訪問(wèn)框架的事實(shí)標(biāo)準(zhǔn),而AFNetworking使用的是block來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)回調(diào),block會(huì)讓block里面引用的變量的引用數(shù)+1,在某種網(wǎng)速非常緩慢的極端情況下,當(dāng)用戶打開ViewController的時(shí)候,網(wǎng)絡(luò)請(qǐng)求已經(jīng)發(fā)出,也就是說(shuō)在block中的變量引用已經(jīng)+1,如果此時(shí)用戶退出這個(gè)ViewController,當(dāng)這個(gè)block發(fā)生回調(diào)時(shí),此時(shí)持有block里面變量的ViewController已經(jīng)被回收,而block里面的變量由于block的原因沒(méi)有被及時(shí)回收,這樣會(huì)造成crash的。這是在使用block進(jìn)行回調(diào)時(shí)很容易被忽略的情況。
對(duì)于業(yè)務(wù)而言,將所有業(yè)務(wù)不加區(qū)分都放進(jìn)ViewController中,這顯然是一種懶惰的表現(xiàn)。一個(gè)ViewController中包含大量業(yè)務(wù)的細(xì)節(jié),將使這個(gè)ViewController在業(yè)務(wù)協(xié)調(diào)和調(diào)用中迷失,將讓這個(gè)業(yè)務(wù)變得非?;靵y,讓業(yè)務(wù)邏輯變得無(wú)法維護(hù)。由于重量級(jí)ViewController的復(fù)雜性,其代碼將難以復(fù)用。
以上原因是傳統(tǒng)MVC難以解決的,為解決這些問(wèn)題,要采用MVVM的開發(fā)模式。
MVVM不是什么新鮮事,簡(jiǎn)單說(shuō)就是將部分邏輯從ViewController中拆分出來(lái),并整合起來(lái)在ViewController和Model中間加多一個(gè)ViewModel,ViewModel不直接引用View,ViewController也不引用Model中的方法,所有網(wǎng)絡(luò)回調(diào)數(shù)據(jù)處理等邏輯都放到ViewModel中,ViewController通過(guò)ViewModel來(lái)請(qǐng)求數(shù)據(jù)和更新數(shù)據(jù)。
如何實(shí)踐MVVM
參考項(xiàng)目:https://github.com/britzlieg/MVVMDemo/tree/master
第一步:創(chuàng)建Model
AFNetworking請(qǐng)求方法放到Model中
@interface Model : NSObject
@property (nonatomic, copy) NSString *col;
@property (nonatomic, copy) NSString *sort;
@property (nonatomic, copy) NSString *tag3;
@property (nonatomic, assign) NSInteger startIndex;
@property (nonatomic, assign) NSInteger returnNumber;
@property (nonatomic, strong) NSArray *imgs;
@property (nonatomic, copy) NSString *tag;
@property (nonatomic, assign) NSInteger totalNum;
+ (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail;
具體實(shí)現(xiàn),不多說(shuō):
@implementation Model
+ (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail {
NSString *urlString = [NSString stringWithFormat:@"%@%ld%@",
@"http://image.baidu.com/data/imgs?col=%e7%be%8e%e5%a5%b3&tag=%e5%b0%8f%e6%b8%85%e6%96%b0&sort=0&pn=1",
aPage,@"&rn=1&p=channel&from=1"];
AFHTTPRequestOperationManager *managere = [AFHTTPRequestOperationManager manager];
[managere GET:urlString parameters:nil
success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) {
success(responseObject,nil);
NSLog(@"success");
} failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) {
fail(nil,error);
NSLog(@"fail");
}];
}
@end
第二步:創(chuàng)建ViewModel
ViewModel的屬性:
- data : 請(qǐng)求獲取的數(shù)據(jù)
- racMsg : 請(qǐng)求成功和失敗的信號(hào)量(主要用KVO對(duì)這個(gè)進(jìn)行監(jiān)視)
@interface ViewModel : NSObject
@property (strong,nonatomic) NSDictionary *data;
@property (strong,nonatomic) NSString *racMsg;
- (void)getImagesList;
- (void)getNextImagesList;
- (void)getPreImagesList;
@end
ViewController中主要監(jiān)視ViewModel的racMsg來(lái)發(fā)現(xiàn)data更新。
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;
@interface ViewModel()
@property (nonatomic) NSInteger currentPage;
@end
@implementation ViewModel
- (instancetype)init {
self = [super init];
self.currentPage = 0;
return self;
}
- (void)getImagesList {
WS(ws)
[Model getImagesListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
- (void)getNextImagesList {
WS(ws)
self.currentPage++;
[Model getImagesListWithPage:self.currentPage
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
- (void)getPreImagesList {
WS(ws)
self.currentPage = self.currentPage == 0 ? 0 : self.currentPage-1;
[Model getImagesListWithPage:self.currentPage
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
@end
可能會(huì)有人覺(jué)得為什么不直接監(jiān)視data,但我個(gè)人更傾向于采用一種類似于信號(hào)量的機(jī)制,監(jiān)聽特定的信號(hào)來(lái)更新數(shù)據(jù)。如果直接監(jiān)視data,則data只有nil和非nil兩種情況,要進(jìn)一步區(qū)分請(qǐng)求的狀態(tài)的話必須要對(duì)data進(jìn)行解析,增加了轉(zhuǎn)換成本,不如直接采用多一個(gè)屬性變量進(jìn)行判斷和協(xié)調(diào)。
第三步:ViewController中KVO設(shè)置
ViewController直接持有viewModel
@interface ViewController ()
@property (strong,nonatomic) ViewModel *viewModel;
@property (strong,nonatomic) UITextView *showTextView;
@end
加載ViewController時(shí)初始化KVO和調(diào)用ViewModel方法getImagesList來(lái)請(qǐng)求數(shù)據(jù)
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// requestData
[self _initViews];
[self setupKVO];
[self.viewModel getImagesList];
}
ViewController銷毀時(shí)去除KVO
- (void)dealloc {
[self removeKVO];
}
KVO相關(guān)的函數(shù)。observeValueForKeyPath只需對(duì)racMsg進(jìn)行判斷就可以知道data的值是否更新了,如果更新了就更新一下View。
#pragma mark - KVO
- (void)setupKVO {
[self.viewModel addObserver:self
forKeyPath:@"racMsg" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
}
- (void)removeKVO {
[self.viewModel removeObserver:self forKeyPath:@"racMsg"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
}
else {
_showTextView.text = @"error";
}
}
}
按鈕點(diǎn)擊事件和View的初始化
#pragma mark - Event Response
- (void)getPre {
[self.viewModel getPreImagesList];
}
- (void)getNext {
[self.viewModel getNextImagesList];
}
#pragma mark - Private
- (void)_initViews {
UIButton *preBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 50, 200, 40)];
[preBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[preBtn setTitle:@"Pre" forState:UIControlStateNormal];
[preBtn addTarget:self action:@selector(getPre) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:preBtn];
UIButton *nextBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 150, 200, 40)];
[nextBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
[nextBtn setTitle:@"nextBtn" forState:UIControlStateNormal];
[nextBtn addTarget:self action:@selector(getNext) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:nextBtn];
_showTextView = [[UITextView alloc]initWithFrame:CGRectMake(0, 200, 320, 200)];
_showTextView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:_showTextView];
}
實(shí)踐MVVM的具體好處的例子
下面討論一下用MVVM的具體好處的例子。
case 1:ViewController需要一個(gè)額外請(qǐng)求一個(gè)文章列表,這個(gè)文章列表的請(qǐng)求參數(shù)與當(dāng)前請(qǐng)求圖片列表的接口返回結(jié)果沒(méi)有任何關(guān)聯(lián),可以并行請(qǐng)求。
在這種情況下,在ViewModel中加入方法getArticleList()和屬性articleList以及articleMsg,然后在ViewController中需要調(diào)用該方法的位置調(diào)用該方法即可。KVO中的observeValueForKeyPath方法稍微修改一下:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
}
else {
_showTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"articleMsg"]) {
if ([_viewModel.articleMsg isEqualToString:@"success"]) {
_articleTextView.text = _viewModel.articleList
}
else {
_articleTextView.text = @"error";
}
}
}
可見(jiàn)并行業(yè)務(wù)功能上的擴(kuò)展是非常簡(jiǎn)單的,整個(gè)Controller的總體邏輯幾乎不用怎么變化,只需要改動(dòng)局部細(xì)節(jié)即可。
case 2:同case 1,但是文章列表的請(qǐng)求參數(shù)需要通過(guò)圖片列表接口返回的結(jié)果獲取,請(qǐng)求是串聯(lián)嵌套的(即先請(qǐng)求圖片列表接口,請(qǐng)求完成后,根據(jù)返回結(jié)果再請(qǐng)求文章列表接口)。
對(duì)于嵌套的請(qǐng)求,如果采用傳統(tǒng)MVC模式,就要在ViewController中加入兩個(gè)block,一個(gè)嵌套另外一個(gè),這樣會(huì)讓代碼變得非常難看,而且會(huì)讓子block依賴于父block,難以對(duì)其進(jìn)行拆分。但如果采用MVVM,則會(huì)將所有的請(qǐng)求變化都置于KVO的監(jiān)控之下,并作出統(tǒng)一的處理。
ViewModel中的與case 1一樣,但ViewController中的處理稍微不同。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
// 請(qǐng)求文章
[_viewModel getArticleList];
}
else {
_showTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"articleMsg"]) {
if ([_viewModel.articleMsg isEqualToString:@"success"]) {
_articleTextView.text = _viewModel.articleList
}
else {
_articleTextView.text = @"error";
}
}
}
可以看出還是不需要改動(dòng)大邏輯,即可對(duì)有依賴的業(yè)務(wù)進(jìn)行擴(kuò)展。
case 3: 同case 2,但是增加一個(gè)依賴于文章列表返回結(jié)果的評(píng)估列表接口(即圖片->文章->評(píng)論)。
假設(shè)存在一種這樣的情況,請(qǐng)求數(shù)據(jù)的順序是:圖片->文章->評(píng)論,這樣的話如果用傳統(tǒng)的MVC模式做的,就是三層block的嵌套,這對(duì)于一個(gè)ViewController來(lái)說(shuō)是噩夢(mèng)。三層嵌套,意味著無(wú)法復(fù)用,只能寫死在這個(gè)ViewController之中。這時(shí)使用MVVM就顯得非常必要了。
當(dāng)出現(xiàn)三層以上的依賴時(shí),其實(shí)可以考慮將所有依賴?yán)壴谝黄穑龀梢粋€(gè)高內(nèi)聚的模塊,在MVVM中,由于請(qǐng)求數(shù)據(jù)的方法不是寫在ViewController之中,所以將這三個(gè)模塊進(jìn)行內(nèi)聚的工作是放到ViewModel之中。
ViewModel.m :
- (void)getCommentsList {
WS(ws)
[Model getImagesListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
[Model getArticlesListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.article = responseObjectDict;
ws.articleMsg = @"success";
[Model getCommentsListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.comments = responseObjectDict;
ws.commentsMsg = @"success"; // 成功
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.comments = nil;
ws.commentsMsg = @"fail"; // 失敗
}];
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.article = nil;
ws.articleMsg = @"fail";
}];
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
ViewController.h中:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
// 請(qǐng)求文章
[_viewModel getArticleList];
}
else {
_showTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"articleMsg"]) {
if ([_viewModel.articleMsg isEqualToString:@"success"]) {
_articleTextView.text = _viewModel.articleList
}
else {
_articleTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"commentsMsg"]) {
if ([_viewModel.commentsMsg isEqualToString:@"success"]) {
_commentsTextView.text = _viewModel.commentsList
}
else {
_commentsTextView.text = @"error";
}
}
}
三層嵌套的block是無(wú)法避免的,但是MVVM可以將這個(gè)惡心的東西放到ViewModel中,而不是ViewController中,這樣當(dāng)ViewController要獲取評(píng)論時(shí),只需要調(diào)用viewModel的getCommentsList即可,不需要看到三層block請(qǐng)求的細(xì)節(jié),這樣可以很好的將邏輯與細(xì)節(jié)隔離。
可以看出使用MVVM還是能夠很方便的擴(kuò)展多層依賴的業(yè)務(wù)。
case 4: 另外一個(gè)ViewController需要調(diào)用圖片接口獲取數(shù)據(jù)
這種情況非常簡(jiǎn)單,直接在ViewController2中加入一個(gè)ViewModel的屬性,其他按照ViewController中的調(diào)用方式調(diào)用即可。
由于將請(qǐng)求邏輯放到了ViewController,所以ViewModel對(duì)ViewController是沒(méi)有依賴的,所以ViewController2能夠很好的直接使用ViewModel,這是傳統(tǒng)MVC很難做得到的。
case 5: 另外一個(gè)ViewController需要評(píng)論接口來(lái)獲取數(shù)據(jù)(將三個(gè)接口請(qǐng)求過(guò)程內(nèi)聚,便于其他復(fù)用)
這種情況也非常簡(jiǎn)單,與case 4是一樣的,直接調(diào)用ViewModel中的getCommentsList(),而這個(gè)函數(shù)是已經(jīng)在ViewModel中高內(nèi)聚了,所以使用也非常方便,代碼復(fù)用和請(qǐng)求邏輯復(fù)用都非常方便清晰!
case 6: ViewController中有很多數(shù)據(jù)轉(zhuǎn)換邏輯,多個(gè)ViewController的數(shù)據(jù)轉(zhuǎn)換邏輯都相同的情況。
ViewModel中不僅只包含請(qǐng)求邏輯,還可以包含數(shù)據(jù)轉(zhuǎn)換的邏輯,還有一些不知要怎么歸類的雜七雜八的邏輯。多個(gè)ViewController發(fā)生相同的數(shù)據(jù)轉(zhuǎn)換的情況是經(jīng)常會(huì)有的,如果把數(shù)據(jù)轉(zhuǎn)換邏輯寫到Controller之中,會(huì)讓每一個(gè)Controller都持有一個(gè)轉(zhuǎn)換邏輯,這對(duì)于數(shù)據(jù)轉(zhuǎn)換邏輯的統(tǒng)一來(lái)說(shuō)是非常糟糕的。如果把相同的數(shù)據(jù)轉(zhuǎn)換邏輯都抽象封裝到同一個(gè)ViewModel中,ViewController不直接持有數(shù)據(jù)轉(zhuǎn)換邏輯,而是通過(guò)ViewModel來(lái)調(diào)用的話,每個(gè)ViewController只需要維護(hù)一個(gè)ViewModel實(shí)例即可,所有轉(zhuǎn)換細(xì)節(jié)都可以在ViewModel中進(jìn)行統(tǒng)一修改。Controller只關(guān)心數(shù)據(jù)和數(shù)據(jù)與View的交互,不應(yīng)該關(guān)心數(shù)據(jù)之間的轉(zhuǎn)換和數(shù)據(jù)怎樣獲取的,這應(yīng)該是MVVM的一個(gè)原則。
總結(jié)
MVVM的核心在于綁定,本文采用的是KVO的綁定機(jī)制,能夠很好與Objective-C和Cocoa結(jié)合起來(lái),不需要借用第三方的類庫(kù)進(jìn)行數(shù)據(jù)綁定。
除了使用KVO,業(yè)界通常采用的是ReactiveCocoa。但是,ReactiveCocoa的學(xué)習(xí)成本過(guò)高,不適合輕量級(jí)的開發(fā),而MVVM只是一種開發(fā)模式,并不是一種具體的框架,所以如果不是非常想深入使用MVVM的精髓的話,是沒(méi)有必要去學(xué)習(xí)ReactiveCocoa的。網(wǎng)上還有一些討論MVVM的博客提到ViewModel直接對(duì)View進(jìn)行操作,其實(shí)這是一種很不嚴(yán)謹(jǐn)?shù)淖龇ǎ琈VVM中的ViewModel不應(yīng)該關(guān)心View的顯示,只應(yīng)該關(guān)心數(shù)據(jù)的獲取和轉(zhuǎn)換,View如何顯示那是ViewController的職責(zé)。所以凡是在ViewModel中引用了UIKit的,個(gè)人認(rèn)為都不是一種嚴(yán)格意義上的MVVM。
當(dāng)然MVVM也有其自身的不足,比如引入ViewModel之后,文件數(shù)量增加了不少,總的代碼量其實(shí)也會(huì)增加,這對(duì)于極簡(jiǎn)主義者來(lái)說(shuō)并不是一種很好的模式。而且MVVM的開發(fā)思路與MVC是不同的,開發(fā)者要轉(zhuǎn)換思路采用MVVM的開發(fā)方式其實(shí)還是有不少的學(xué)習(xí)成本,而且對(duì)于大部分簡(jiǎn)單業(yè)務(wù)來(lái)說(shuō),使用MVVM會(huì)增加業(yè)務(wù)的復(fù)雜度,顯得臃腫和多余。本文中使用KVO的MVVM模式,從本質(zhì)上來(lái)說(shuō)其實(shí)是MVC的衍生,把C中的一部分拆分出來(lái)并隔離M和C,所以從模式上來(lái)說(shuō),這樣是完全可以兼容傳統(tǒng)的MVC開發(fā)模式的。因此,對(duì)于簡(jiǎn)單的業(yè)務(wù),可以直接采用MVC的模式開發(fā),不需要額外創(chuàng)建一個(gè)ViewModel。對(duì)于復(fù)雜業(yè)務(wù),就采用KVO的MVVM模式,進(jìn)行業(yè)務(wù)拆分和復(fù)用。這種折中的方法能夠?qū)VC和MVVM的優(yōu)點(diǎn)都利用起來(lái),避免只使用一個(gè)造成開發(fā)效率上的降低。
MVVM不應(yīng)該被誤解和神化,使用MVVM只是提供多了一個(gè)不錯(cuò)的選擇,要不要使用它,還是要看具體的項(xiàng)目而定。但是用上了,就停不下來(lái)了。