iOS开发中 MVVM 设计模式的探究

前言

一直在做一线的业务开发工作,每天接触业务线,时间久了就开始思考如何能优化架构、提高维护效率,于是就接触了MVVM

MVVM的出现主要是为了解决在开发过程中Controller越来越庞大的问题,变得难以维护,所以MVVM把数据加工的任务从Controller中解放了出来,使得Controller只需要专注于具体业务的工作,ViewModel则去负责数据加工并通过各种通知机制让View响应ViewModel的改变。

还有一个让人很容易忽略的问题,大部分国内外资料阐述MVVM的时候都是这样排布的:Model - View - ViewModel ,
客观上造成了MVVM不需要Controller的错觉,可事实上是这样的么?

分析使用逻辑

ViewModel部分

我最开始听说MVVM的时候是一脸懵逼的,因为我在这几个子母中间没有看到Controller的影子,我对于Controller的使用逻辑一无所知。但是直觉告诉我,Controller依然还是不可获取的一部分,至少操作View的时候还是会需要。ViewModel这个单词本身也让我产生了困扰,这个术语本身可能导致混乱,因为它是我们已经知道的两个术语的混搭,不知道这个结构更偏向于ViewModel哪一边。

当我查阅资料的时候,网上大部分的MVVM的表述都是View <-> ViewModel <-> Model,都是在说ViewModel支持数据的双向绑定,表述模糊,我还是不能完全明白ViewModel的真正用处和存在意义。

比如像下面的说法:

它将model信息转变为view需要的信息,同时还将命令从view传递到model。View与Model通过ViewModel实现双向绑定。

我的着眼点放在了上面的第一句话上,即“它将model信息转变为view信息,同时还将命令从view传递到model”。当我进一步去理解ViewModel的作用的时候,我发现ViewModel更应该被称呼作为“View与Model中间的观察协调器”,它主要作用是拿到原始的数据,根据具体业务逻辑需要进行处理,之后将处理好的东西塞到View中去,其职责之一是静态模型,表示View显示自身所需的数据,这使View具有更清晰定义的任务,即呈现视图模型提供的数据,总结为一句话就是与View直接对应的Model,逻辑上是属于Model层

Controller部分

那么Controller的作用呢?

MVVM其实是是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:Model和ViewModel。
MVC -> 胖Model -> Model+ ViewModel这个演化过程。

和上文分析一致,ViewModel本质上算是Model层(作为与View显示自己所直接对应的Model),所以View并不适合直接持有ViewModel,反之则可以。因为ViewModel有可能并不是一定只服务于特定的一个View,使用更加松散的绑定关系能够降低ViewModel和View之间的耦合度。现在可以说,Controller唯一关注的是使用来自ViewModel的数据配置和管理各种View,Controller的作用就是负责控制ViewViewModel的绑定和调用关系,Controller不需要了解Web服务调用,Core Data,model对象等,这些都是可以丢给ViewModel去操作。

以下2张图或许有更直观的感受:

图片来源:http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/

我来解释一下图片指代的意思:

  1. MVVM中的View本质上代指的是View和Controller两部分结构。
  2. 而MVVM中的ViewModel则是抽离了Controller的一些业务出来组成了ViewModel,同时Controller的工作量减小了很多。
  3. 当Controller抽离出来ViewModel之后,MVVM中View也可以区分为View和Controller两部分之后,就成了第二张图MVCVM的结构,Controller的代销也就变小了。

所以说,归根结底MVVM更应该被形容为View <-> C <-> ViewModel <-> Model,即MVCVM的设计模式,这样就很清晰的解释了Controller的位置以及作用。

进一步梳理

通过以上,我们可以逻辑上得出,Controller应该是持有了ViewModel和View,并对二者进行判断和匹配操作。ViewModel有可能并不是一定只服务于特定的一个View,Controller当中也会有很多的View,所以,同一个Controller可能持有很多个ViewModel,以此来实现不同的业务逻辑控制多重的View。

Controller唯一关注的是使用来自ViewModel的数据配置和管理各种View,并让ViewModel知道何时发生需要更改上游数据的相关用户输入。 Controller不需要知道网络请求、数据库操作以及Model等,这样就让Controller更集中的去处理具体的业务逻辑。

Free-Converter.com-205842hoe46esq6u55noqd-60352002.png

图片来源:http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/

  • Controller:负责View和ViewModel之间的调配和绑定关系,执行业务逻辑。
  • View:展示UI,接受Action。
  • ViewModel:网络请求,数据加工,数据持有。
  • Model:原始数据。

大致会形成以下调用关系:


图片一

图片二

代码演示

废话不多说,先上Demo:https://github.com/derekhuangxu/HXMVVMDemo

下面我会摘出部分重要代码,给大家讲解一下具体的逻辑

MarshalModel

MarshalModel.h
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger lessonCount;

Model相对来说就比较简单了,用来储存原始数据。


MarshalViewModel

MarshalViewModel.h

@interface MarshalViewModel : NSObject
@property (nonatomic, strong, readonly) NSString *name;
@property (nonatomic, strong, readonly) NSString *detailTextString;
@property (nonatomic, assign, readonly) UITableViewCellAccessoryType cellType;
- (instancetype)initWithModel:(MarshalModel *)model;
@end

MarshalViewModel.m

@interface MarshalViewModel ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *detailTextString;
@property (nonatomic, assign) UITableViewCellAccessoryType cellType;
@end

@implementation MarshalViewModel

//依赖注入
- (instancetype)initWithModel:(MarshalModel *)model ; {
    if (self = [super init]) {
        self.name = model.name;
        if (model.lessonCount > 35) {
            self.detailTextString = @"多于30门课程";
            self.cellType = UITableViewCellAccessoryDetailDisclosureButton;
        } else {
            self.detailTextString = [NSString stringWithFormat:@"课程数量:%ld", (long)model.lessonCount];
            self.cellType = UITableViewCellAccessoryNone;
        }
    }
    return self;
}
  • ViewModel的.h文件中,只放置了初始化的代码,执行Model数据的注入,另外提供了向View暴露的接口,这部分要注意了,要加上readonly的标示符,当然也可以用GET方法代替,这样可以避免View修改ViewModel中的数据,保证数据从一条线注入,处理之后,从另一条线取出
  • ViewModel的.m文件,可以说是对于原始数据的处理操作,这里基本上就是对于原始数据转化为View需要数据的处理。当然,大家也可以根据自己的需要增加Cache或者数据库等的操作,也可以增加网络数据获取的操作。

MarshalCell

MarshalCell.h
@interface MarshalCell : UITableViewCell
@property (nonatomic, strong) MarshalViewModel *viewModel;
@end

MarshalCell.m
- (void)setViewModel:(MarshalViewModel *)viewModel {
    _viewModel = viewModel;
    self.textLabel.text = viewModel.name;
    self.detailTextLabel.text = viewModel.detailTextString;
    self.accessoryType = viewModel.cellType;
}

我只把.m文件中的SET方法给大家展示出来了,我觉得就可以说明问题,ViewModel中提供了View需要的一切处理好的关键要素,那么View就会变得非常扁平,只需要处理点击这种的Action事件,并且传递给Controller即可,不需要关注原始数据转换的具体业务逻辑。


HomeViewController

HomeViewController.m

@interface HomeViewController () 
@property (nonatomic, strong) NSMutableArray <MarshalViewModel *>*viewModels;
@end

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    MarshalCell *cell = [[MarshalCell alloc] initWithStyle: UITableViewCellStyleSubtitle
                           reuseIdentifier: kMarshalCellIdentifier];
    cell.viewModel = self.viewModels[indexPath.row];
    return cell;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.viewModels.count;
}

- (void)getData {
    [[MarshalService shareInstance]fetchDataCompletion:^(NSArray<MarshalModel *> * _Nonnull data) {
        self.viewModels = [self viewModelsWithDatas:[data mutableCopy]];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];
        });
    } failure:^(NSError * _Nonnull error) {
        NSLog(@"Failed to fetch courses:%@", error);
    }];
}

大家可以看到,我用了数组来储存的ViewModel,原因是因为每个Cell所对应的的数据不一致导致UI展示不一致,所以每个Cell要有一个自己的ViewModel来填充自己UI数据。这就进一步印证了上面说的,ViewModel本质仍然是Model层

疑问点(不定期更新)

1、为什么View没有.h接口的可读处理,而ViewModel中要有?

在ViewModel中数据单向注入,之后会有数据根据业务逻辑处理,之后向View提供数据接口,为了保证数据不会被污染,所以增加了`readonly`的标示符,而View中则不需要以上的逻辑。

2、当点击某个Cell的某部分之后,如何向后传值?

1.Controller找到对应的ViewModel,ViewModel作为Mother ViewModel,替代Controller做一个脏活,去生成相对应的Child ViewModel。
2.ViewModel将Child ViewModel返回给Controller。
3.Controller利用Child ViewModel生成下一页面的Controller,执行跳转操作。

Refrence

另外

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

推荐阅读更多精彩内容