iOS 从MVP到MVVM

iOS 从MVP到MVVM

前面说到了iOS 从MVC到MVP,最后说到:如果到时候业务复杂、逻辑复杂,更新界面的方法有多个(弹框、菊花等等的),可以通过代理的多个方法实现。这样当然可以,但有没有更简单直接明了的方法呢?接着就产生了MVVM。
本文我们先通过实现一个小功能来了解MVVM:
有必要的请先下载Demo

功能效果图

每一项数据的变化都会反应在右上角的合计上。数据可以增、删、减。
我分别用MVP和MVVM实现了相关的功能。
代码框架

其中代码的共用部分LMDataSource是应用代理模式来对VC中tableView代码的提取,VC中添加TableView只需要一句代码即可搞定:

//-------------tableView相关操作
/**
     cellConfigureBefore:cell的数据设置
     selectBlock: 点击cell的回掉
     reloadData: 删除数据、添加数据后的回掉
     */
    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(@"点击了%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的点击代理方法

- (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的实现

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":@"飞机",@"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++) {
        // 查数据 ---> 钱
        if (i == indexPath.row) {// 商品ID 容错
            Model *m = self.dataArray[indexPath.row];
            m.num    = num;
            break;
        }
    }
    
    if ([num intValue] > 6) {
        NSArray *temArray =
        @[
          @{@"name":@"火车",@"imageUrl":@"http://CC",@"num":@"6"},
          @{@"name":@"飞机",@"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 计算总数
-(int)total{
    int total = 0;
    for (Model* dic in self.dataArray) {
        int num = [dic.num intValue];
        total += num;
    }
    return total;
}

代码中除去初始化、获取数据,就只剩下cell的代理实现。其中在cell的代理实现中数据处理完成后,又通过一个代理让VC刷新数据
通过代理,让VC来刷新数据

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相关操作
    /**
     cellConfigureBefore:cell的数据设置
     selectBlock: 点击cell的回掉
     reloadData: 删除数据、添加数据后的回掉
     */
    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(@"点击了%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代理设置
    self.pt.delegate          = self;
    
    self.navigationItem.rightBarButtonItem.title = [NSString stringWithFormat:@"合计:%d",[self.pt total]];
}

#pragma mark - PresentDelegate
- (void)reloadDataForUI{
    [self.dataSource addDataArray:self.pt.dataArray];
    [self.tableView reloadData];
    self.navigationItem.rightBarButtonItem.title = [NSString stringWithFormat:@"合计:%d",[self.pt total]];
}

代码很简单,其中包含:tableView的处理,Present的设置及其代理的设置,然后就是Present代理方法实现。整体结构还算清晰。
但有两点,第一:reloadDataForUI这里,有两个地方调用,一个是代理,一个是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.异步执行任务
    dispatch_async(q, ^{
        
        NSArray *temArray = @[
                              @{@"name":@"火车",@"imageUrl":@"http://CC",@"num":@"1"},
                              @{@"name":@"飞机",@"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++) {
        // 查数据 ---> 钱
        if (i == indexPath.row) {// 商品ID 容错
            Model *m = self.dataArray[indexPath.row];
            m.num    = num;
            break;
        }
    }
    
    
    if ([num intValue] > 6) {
        NSArray *temArray =
        @[
          @{@"name":@"火车",@"imageUrl":@"http://CC",@"num":@"6"},
          @{@"name":@"飞机",@"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的监听,监听到变化以后调用self.successBlock()。还有一点,cell的代理实现方法中,数据处理完以后,也是调用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:@"合计:" style:(UIBarButtonItemStyleDone) target:self action:nil];
    
    __weak __typeof(self) weakSelf = self;
    
    //-------------tableView相关操作
    /**
     cellConfigureBefore:cell的数据设置
     selectBlock: 点击cell的回掉
     reloadData: 删除数据、添加数据后的回掉
     */
    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(@"点击了%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:@"合计:%d",[weakSelf.vm total]];
        
        
    } fail:nil];
    
    
    //加载数据
    [self.vm loadData];

}


-(void)dealloc{
    NSLog(@"dealloc--%@",self);
}

什么,如此简单,代码就在ViewDidLoad里面实现完了。
对比MVP,Present变成了ViewModel,前面Present代理方法刷新数据变成了VM的initWithBlock
LMDataSource中reloadData中只设置了ViewModel的dataArray的值。为什么都不用调用刷新界面的方法呢,原来在ViewModel设置了对dataArray的监听,只要监听到dataArray变化,就会执行VM的self.successBlock(),也就是进入了VC的VM的initWithBlock中。完美。

MVVM

敲黑板

通过这个案例并结合这张图,我们可以看出:当View|Controller有用户交互,就通过响应告诉ViewModel,然后Model改变,而ViewModel会监听到Model的改变然后通过Success回掉来更新UI


点击事件传递流程图

3、MVVM 的基本概念

在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件

view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel)

viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方

使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性

MVVM 的注意事项

view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)

viewModel 引用model,但反过来不行

MVVM 的使用建议

MVVM 可以兼容你当下使用的MVC架构。

MVVM 增加你的应用的可测试性。

MVVM 配合一个绑定机制效果最好(PS:ReactiveCocoa你值得拥有)。

viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。

viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。

viewModel 绝对不能包含视图 view(UIKit.h),不然就跟 view 产生了耦合,不方便复用和测试。

viewModel之间可以有依赖。

viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。

MVVM 的优势

低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上

可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑

独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计

可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试

MVVM 的弊端

数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。

对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:

数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。

转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。

只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。

调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。

同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。

如果觉得有用可以下载Demo了解更多。
如果对LMDataSource这个类的功能不太了解,这里是使用了代理模式,可以看看设计模式--代理模式(iOS)
如果对MVP不太了解,可以看看iOS 从MVC到MVP

写在最后:
希望这篇文章对您有帮助。当然如果您发现有可以优化的地方,希望您能慷慨的提出来。最后祝您工作愉快!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容