前面說到了iOS 從MVC到MVP,最后說到:如果到時(shí)候業(yè)務(wù)復(fù)雜、邏輯復(fù)雜,更新界面的方法有多個(gè)(彈框、菊花等等的),可以通過代理的多個(gè)方法實(shí)現(xiàn)。這樣當(dāng)然可以,但有沒有更簡單直接明了的方法呢?接著就產(chǎn)生了MVVM。
本文我們先通過實(shí)現(xiàn)一個(gè)小功能來了解MVVM:
有必要的請先下載Demo
每一項(xiàng)數(shù)據(jù)的變化都會(huì)反應(yīng)在右上角的合計(jì)上。數(shù)據(jù)可以增、刪、減。
我分別用MVP和MVVM實(shí)現(xiàn)了相關(guān)的功能。
其中代碼的共用部分LMDataSource
是應(yīng)用代理模式來對VC中tableView代碼的提取,VC中添加TableView只需要一句代碼即可搞定:
//-------------tableView相關(guān)操作
/**
cellConfigureBefore:cell的數(shù)據(jù)設(shè)置
selectBlock: 點(diǎn)擊cell的回掉
reloadData: 刪除數(shù)據(jù)、添加數(shù)據(jù)后的回掉
*/
self.dataSource = [[LMDataSource alloc] initWithIdentifier:reuserId configureBlock:^(MVPTableViewCell *cell, Model *model, NSIndexPath *indexPath) {
cell.nameLabel.text = model.name;
cell.numLabel.text = model.num;
cell.num = [model.num intValue];
cell.indexPath = indexPath;
cell.delegate = weakSelf.vm;
} selectBlock:^(NSIndexPath *indexPath) {
NSLog(@"點(diǎn)擊了%ld行cell", (long)indexPath.row);
} reloadData:^(NSMutableArray *array) {
weakSelf.vm.dataArray = array;
}];
self.tableView.dataSource = self.dataSource;
self.tableView.delegate = self.dataSource;
MVPTableViewCell
是自定義cell,PresentDalegate
是cell的點(diǎn)擊代理方法
- (void)didClickAddBtn:(UIButton *)sender{
if ([self.numLabel.text intValue]>=200) {return;}
self.num++;
if (self.delegate && [self.delegate respondsToSelector:@selector(didClickAddBtnWithNum:indexPath:)]) {
[self.delegate didClickAddBtnWithNum:self.numLabel.text indexPath:self.indexPath];
}
}
1、MVP的實(shí)現(xiàn)
1.1 MVP中的Present
#pragma mark - lazy
- (NSMutableArray *)dataArray{
if (!_dataArray) {
_dataArray = [NSMutableArray arrayWithCapacity:10];
}
return _dataArray;
}
- (instancetype)init{
if (self = [super init]) {
[self loadData];
}
return self;
}
- (void)loadData{
NSArray *temArray =
@[
@{@"name":@"火車",@"imageUrl":@"http://CC",@"num":@"1"},
@{@"name":@"飛機(jī)",@"imageUrl":@"http://James",@"num":@"1"},
@{@"name":@"跑車",@"imageUrl":@"http://Gavin",@"num":@"1"},
@{@"name":@"女票",@"imageUrl":@"http://Cooci",@"num":@"1"},
@{@"name":@"男票",@"imageUrl":@"http://Dean ",@"num":@"1"},
@{@"name":@"滑板",@"imageUrl":@"http://CC",@"num":@"1"},
@{@"name":@"一日游",@"imageUrl":@"http://James",@"num":@"1"}];
for (int i = 0; i<temArray.count; i++) {
Model *m = [Model modelWithDictionary:temArray[i]];
[self.dataArray addObject:m];
}
}
#pragma mark - PresentDelegate
- (void)didClickAddBtnWithNum:(NSString *)num indexPath:(NSIndexPath *)indexPath{
for (int i = 0; i<self.dataArray.count; i++) {
// 查數(shù)據(jù) ---> 錢
if (i == indexPath.row) {// 商品ID 容錯(cuò)
Model *m = self.dataArray[indexPath.row];
m.num = num;
break;
}
}
if ([num intValue] > 6) {
NSArray *temArray =
@[
@{@"name":@"火車",@"imageUrl":@"http://CC",@"num":@"6"},
@{@"name":@"飛機(jī)",@"imageUrl":@"http://James",@"num":@"6"},
@{@"name":@"跑車",@"imageUrl":@"http://Gavin",@"num":@"6"},
@{@"name":@"女票",@"imageUrl":@"http://Cooci",@"num":@"6"},
@{@"name":@"男票",@"imageUrl":@"http://Dean ",@"num":@"6"},
@{@"name":@"滑板",@"imageUrl":@"http://CC",@"num":@"6"},
@{@"name":@"一日游",@"imageUrl":@"http://James",@"num":@"6"}];
[self.dataArray removeAllObjects];
for (int i = 0; i<temArray.count; i++) {
Model *m = [Model modelWithDictionary:temArray[i]];
[self.dataArray addObject:m];
}
}
if (self.delegate && [self.delegate respondsToSelector:@selector(reloadDataForUI)]) {
[self.delegate reloadDataForUI];
}
}
#pragma mark 計(jì)算總數(shù)
-(int)total{
int total = 0;
for (Model* dic in self.dataArray) {
int num = [dic.num intValue];
total += num;
}
return total;
}
代碼中除去初始化、獲取數(shù)據(jù),就只剩下cell的代理實(shí)現(xiàn)。其中在cell的代理實(shí)現(xiàn)中數(shù)據(jù)處理完成后,又通過一個(gè)代理讓VC刷新數(shù)據(jù)1.2 MVP中的V(ViewController)
- (UITableView *)tableView{
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.backgroundColor = [UIColor whiteColor];
[_tableView registerClass:[MVPTableViewCell class] forCellReuseIdentifier:reuserId];
}
return _tableView;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.pt = [[Present alloc] init];
__weak typeof(self) weakSelf = self;
//-------------tableView相關(guān)操作
/**
cellConfigureBefore:cell的數(shù)據(jù)設(shè)置
selectBlock: 點(diǎn)擊cell的回掉
reloadData: 刪除數(shù)據(jù)、添加數(shù)據(jù)后的回掉
*/
self.dataSource = [[LMDataSource alloc] initWithIdentifier:reuserId configureBlock:^(MVPTableViewCell *cell, Model *model, NSIndexPath *indexPath) {
cell.nameLabel.text = model.name;
cell.numLabel.text = model.num;
cell.num = [model.num intValue];
cell.indexPath = indexPath;
cell.delegate = weakSelf.pt;
} selectBlock:^(NSIndexPath *indexPath) {
NSLog(@"點(diǎn)擊了%ld行cell", (long)indexPath.row);
} reloadData:^(NSMutableArray *array) {
weakSelf.pt.dataArray = [array copy];
[weakSelf reloadDataForUI];
}];
[self.dataSource addDataArray:self.pt.dataArray];
self.view.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.tableView];
self.tableView.dataSource = self.dataSource;
self.tableView.delegate = self.dataSource;
//Present代理設(shè)置
self.pt.delegate = self;
self.navigationItem.rightBarButtonItem.title = [NSString stringWithFormat:@"合計(jì):%d",[self.pt total]];
}
#pragma mark - PresentDelegate
- (void)reloadDataForUI{
[self.dataSource addDataArray:self.pt.dataArray];
[self.tableView reloadData];
self.navigationItem.rightBarButtonItem.title = [NSString stringWithFormat:@"合計(jì):%d",[self.pt total]];
}
代碼很簡單,其中包含:tableView的處理,Present的設(shè)置及其代理的設(shè)置,然后就是Present代理方法實(shí)現(xiàn)。整體結(jié)構(gòu)還算清晰。
但有兩點(diǎn),第一:reloadDataForUI
這里,有兩個(gè)地方調(diào)用,一個(gè)是代理,一個(gè)是LMDataSource
的reloadData中需要刷新列表。第二:cell的代理Present,在cell的代理方法里面又包含Present的代理VC。
整體效果不太友好。有沒有更好的辦法呢?于是就有了MVVM。
2、MVVM
2.1、MVVM中的ViewModel
#pragma mark - lazy
- (NSMutableArray *)dataArray{
if (!_dataArray) {
_dataArray = [NSMutableArray arrayWithCapacity:1];
}
return _dataArray;
}
- (instancetype)init{
if (self==[super init]) {
[self addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:nil];
}
return self;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@ change",change[NSKeyValueChangeNewKey]);
self.successBlock(change[NSKeyValueChangeNewKey]);
}
-(void)dealloc{
[self removeObserver:self forKeyPath:@"dataArray"];
}
-(void)loadData{
dispatch_queue_t q = dispatch_queue_create("udpios", DISPATCH_QUEUE_CONCURRENT);
//2.異步執(zhí)行任務(wù)
dispatch_async(q, ^{
NSArray *temArray = @[
@{@"name":@"火車",@"imageUrl":@"http://CC",@"num":@"1"},
@{@"name":@"飛機(jī)",@"imageUrl":@"http://James",@"num":@"1"},
@{@"name":@"跑車",@"imageUrl":@"http://Gavin",@"num":@"1"},
@{@"name":@"女票",@"imageUrl":@"http://Cooci",@"num":@"1"},
@{@"name":@"男票",@"imageUrl":@"http://Dean ",@"num":@"1"},
@{@"name":@"滑板",@"imageUrl":@"http://CC",@"num":@"1"},
@{@"name":@"一日游",@"imageUrl":@"http://James",@"num":@"1"}];
[self.dataArray removeAllObjects];
for (int i = 0; i<temArray.count; i++) {
Model *m = [Model modelWithDictionary:temArray[i]];
[self.dataArray addObject:m];
}
dispatch_async(dispatch_get_main_queue(), ^{
// main更新代碼
self.successBlock(self.dataArray);
});
});
}
#pragma mark - PresentDelegate
- (void)didClickAddBtnWithNum:(NSString *)num indexPath:(NSIndexPath *)indexPath{
for (int i = 0; i<self.dataArray.count; i++) {
// 查數(shù)據(jù) ---> 錢
if (i == indexPath.row) {// 商品ID 容錯(cuò)
Model *m = self.dataArray[indexPath.row];
m.num = num;
break;
}
}
if ([num intValue] > 6) {
NSArray *temArray =
@[
@{@"name":@"火車",@"imageUrl":@"http://CC",@"num":@"6"},
@{@"name":@"飛機(jī)",@"imageUrl":@"http://James",@"num":@"6"},
@{@"name":@"跑車",@"imageUrl":@"http://Gavin",@"num":@"6"},
@{@"name":@"女票",@"imageUrl":@"http://Cooci",@"num":@"6"},
@{@"name":@"男票",@"imageUrl":@"http://Dean ",@"num":@"6"},
@{@"name":@"滑板",@"imageUrl":@"http://CC",@"num":@"6"},
@{@"name":@"一日游",@"imageUrl":@"http://James",@"num":@"6"}];
[self.dataArray removeAllObjects];
for (int i = 0; i<temArray.count; i++) {
Model *m = [Model modelWithDictionary:temArray[i]];
[self.dataArray addObject:m];
}
}
self.successBlock(self.dataArray);
}
-(int)total{
int total = 0;
for (Model* dic in self.dataArray) {
int num = [dic.num intValue];
total += num;
}
return total;
}
對比MVP中的P,代碼變多了,多了對dataArray的監(jiān)聽,監(jiān)聽到變化以后調(diào)用self.successBlock()
。還有一點(diǎn),cell的代理實(shí)現(xiàn)方法中,數(shù)據(jù)處理完以后,也是調(diào)用self.successBlock()
2.2、MVVM中的V(ViewController)
#pragma mark lazy
- (UITableView *)tableView{
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.backgroundColor = [UIColor whiteColor];
[_tableView registerClass:[MVPTableViewCell class] forCellReuseIdentifier:reuserId];
}
return _tableView;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]initWithTitle:@"合計(jì):" style:(UIBarButtonItemStyleDone) target:self action:nil];
__weak __typeof(self) weakSelf = self;
//-------------tableView相關(guān)操作
/**
cellConfigureBefore:cell的數(shù)據(jù)設(shè)置
selectBlock: 點(diǎn)擊cell的回掉
reloadData: 刪除數(shù)據(jù)、添加數(shù)據(jù)后的回掉
*/
self.dataSource = [[LMDataSource alloc] initWithIdentifier:reuserId configureBlock:^(MVPTableViewCell *cell, Model *model, NSIndexPath *indexPath) {
cell.nameLabel.text = model.name;
cell.numLabel.text = model.num;
cell.num = [model.num intValue];
cell.indexPath = indexPath;
cell.delegate = weakSelf.vm;
} selectBlock:^(NSIndexPath *indexPath) {
NSLog(@"點(diǎn)擊了%ld行cell", (long)indexPath.row);
} reloadData:^(NSMutableArray *array) {
weakSelf.vm.dataArray = array;
}];
self.view.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.tableView];
self.tableView.dataSource = self.dataSource;
self.tableView.delegate = self.dataSource;
//--------------ViewModel的操作
self.vm = [[MVVMViewModel alloc]init];
[self.vm initWithBlock:^(id data) {
weakSelf.dataSource.dataArray = [weakSelf.vm.dataArray mutableCopy];
[weakSelf.tableView reloadData];
weakSelf.navigationItem.rightBarButtonItem.title = [NSString stringWithFormat:@"合計(jì):%d",[weakSelf.vm total]];
} fail:nil];
//加載數(shù)據(jù)
[self.vm loadData];
}
-(void)dealloc{
NSLog(@"dealloc--%@",self);
}
什么,如此簡單,代碼就在ViewDidLoad里面實(shí)現(xiàn)完了。
對比MVP,Present變成了ViewModel,前面Present代理方法刷新數(shù)據(jù)變成了VM的initWithBlock
。
LMDataSource
中reloadData中只設(shè)置了ViewModel的dataArray的值。為什么都不用調(diào)用刷新界面的方法呢,原來在ViewModel設(shè)置了對dataArray的監(jiān)聽,只要監(jiān)聽到dataArray變化,就會(huì)執(zhí)行VM的self.successBlock(),也就是進(jìn)入了VC的VM的initWithBlock
中。完美。
敲黑板
通過這個(gè)案例并結(jié)合這張圖,我們可以看出:當(dāng)View|Controller有用戶交互,就通過響應(yīng)告訴ViewModel,然后Model改變,而ViewModel會(huì)監(jiān)聽到Model的改變?nèi)缓笸ㄟ^Success回掉來更新UI
3、MVVM 的基本概念
在MVVM 中,view 和 view controller正式聯(lián)系在一起,我們把它們視為一個(gè)組件
view 和 view controller 都不能直接引用model,而是引用視圖模型(viewModel)
viewModel 是一個(gè)放置用戶輸入驗(yàn)證邏輯,視圖顯示邏輯,發(fā)起網(wǎng)絡(luò)請求和其他代碼的地方
使用MVVM會(huì)輕微的增加代碼量,但總體上減少了代碼的復(fù)雜性
MVVM 的注意事項(xiàng)
view 引用viewModel ,但反過來不行(即不要在viewModel中引入#import UIKit.h,任何視圖本身的引用都不應(yīng)該放在viewModel中)(PS:基本要求,必須滿足)
viewModel 引用model,但反過來不行
MVVM 的使用建議
MVVM 可以兼容你當(dāng)下使用的MVC架構(gòu)。
MVVM 增加你的應(yīng)用的可測試性。
MVVM 配合一個(gè)綁定機(jī)制效果最好(PS:ReactiveCocoa你值得擁有)。
viewController 盡量不涉及業(yè)務(wù)邏輯,讓 viewModel 去做這些事情。
viewController 只是一個(gè)中間人,接收 view 的事件、調(diào)用 viewModel 的方法、響應(yīng) viewModel 的變化。
viewModel 絕對不能包含視圖 view(UIKit.h),不然就跟 view 產(chǎn)生了耦合,不方便復(fù)用和測試。
viewModel之間可以有依賴。
viewModel避免過于臃腫,否則重蹈Controller的覆轍,變得難以維護(hù)。
MVVM 的優(yōu)勢
低耦合:View 可以獨(dú)立于Model變化和修改,一個(gè) viewModel 可以綁定到不同的 View 上
可重用性:可以把一些視圖邏輯放在一個(gè) viewModel里面,讓很多 view 重用這段視圖邏輯
獨(dú)立開發(fā):開發(fā)人員可以專注于業(yè)務(wù)邏輯和數(shù)據(jù)的開發(fā) viewModel,設(shè)計(jì)人員可以專注于頁面設(shè)計(jì)
可測試:通常界面是比較難于測試的,而 MVVM 模式可以針對 viewModel來進(jìn)行測試
MVVM 的弊端
數(shù)據(jù)綁定使得Bug 很難被調(diào)試。你看到界面異常了,有可能是你 View 的代碼有 Bug,也可能是 Model 的代碼有問題。數(shù)據(jù)綁定使得一個(gè)位置的 Bug 被快速傳遞到別的位置,要定位原始出問題的地方就變得不那么容易了。
對于過大的項(xiàng)目,數(shù)據(jù)綁定和數(shù)據(jù)轉(zhuǎn)化需要花費(fèi)更多的內(nèi)存(成本)。主要成本在于:
數(shù)組內(nèi)容的轉(zhuǎn)化成本較高:數(shù)組里面每項(xiàng)都要轉(zhuǎn)化成Item對象,如果Item對象中還有類似數(shù)組,就很頭疼。
轉(zhuǎn)化之后的數(shù)據(jù)在大部分情況是不能直接被展示的,為了能夠被展示,還需要第二次轉(zhuǎn)化。
只有在API返回的數(shù)據(jù)高度標(biāo)準(zhǔn)化時(shí),這些對象原型(Item)的可復(fù)用程度才高,否則容易出現(xiàn)類型爆炸,提高維護(hù)成本。
調(diào)試時(shí)通過對象原型查看數(shù)據(jù)內(nèi)容不如直接通過NSDictionary/NSArray直觀。
同一API的數(shù)據(jù)被不同View展示時(shí),難以控制數(shù)據(jù)轉(zhuǎn)化的代碼,它們有可能會(huì)散落在任何需要的地方。
如果覺得有用可以下載Demo了解更多。
如果對LMDataSource
這個(gè)類的功能不太了解,這里是使用了代理模式,可以看看設(shè)計(jì)模式--代理模式(iOS)
如果對MVP
不太了解,可以看看iOS 從MVC到MVP
寫在最后:
希望這篇文章對您有幫助。當(dāng)然如果您發(fā)現(xiàn)有可以優(yōu)化的地方,希望您能慷慨的提出來。最后祝您工作愉快!