一、概述
- 通过上一篇文章的学习,我们对关于
MVC
的弊端的产生和MVVM
中viewModel
的职责及其使用注意事项,想必都有了些许了解和认识,最起码What is MVC ? What is MVVM ?
,大家也不会感觉这是最熟悉的陌生人了吧。笔者不才,本文将着重谈谈MVVM
在iOS开发中的实际运用,以及自身通过实践探索出来的经验之谈,同时希望能让大家更加深刻体会到MVVM
中M
、V
、VM
各自的职责,以及V
和VM
之间那份剪不断,理还乱的缠绵往事。 - 本文只是笔者在实践
MVVM
过程中的些许见解,在此抛砖引玉,共同探讨下MVVM
的实践思路,希望能够打消你对MVVM
模式的顾虑 ,提供一点思路,少走一些弯路,填补一些细坑。文章仅供大家参考,若有不妥之处,还望不吝赐教,欢迎批评指正。 -
MVVM
基础知识以及其使用注意不了解的,请务必戳我👉 iOS 关于MVC和MVVM设计模式的那些事
二、MVVM
- MVVM的基本概念
-
MVVM的结构图
- MVVM的定义
从上图中,我们可以非常清楚地看到 MVVM 中四个组件之间的关系。注:除了view
、viewModel
和model
之外,MVVM
中还有一个非常重要的隐含组件binder
:
Model :和MVC
中的model
保持一致,完全取决于你的"偏好设置"。你可能会为model
封装一些额外的操作数据的业务逻辑,虽然苹果是推崇你这么干的,但是笔者认为不妥,这样很可能会导致一个胖Model
的产生,而且胖Model相对比较难移植
,胖Model随着产品的迭代会更加的Fat
,最终难以维护,一胖毁所有
。我更倾向于把它当做一个容纳表现数据-模型(data-model
)对象信息的结构体(瘦Model
),并通过一个单独的管理类来维护/创建/管理模型的统一逻辑,又或者可以通过使用Category
来扩充业务逻辑。MVVM是基于胖Model
的架构思路建立的,然后在胖Model
中拆出两部分:Model
和ViewModel
(PS:感觉是否有点道理)。
View:由MVC
中的view
和controller
组成,负责 UI 的展示,绑定viewModel
中的属性,触发viewModel
中的命令以及呈现由viewModel
提供的数据。
View-Model: 千万不要把它与传统数据-模型结构中模型混为一谈。 它的职责之一就是作为一个表现视图显示自身所需数据的静态模型;但它也有收集, 解释和转换那些数据的责任。它是从MVC
的controller
中抽取出来的展示逻辑,负责从model
中获取view
所需的数据,转换成view
可以展示的数据,并暴露公开的属性和命令供view
进行绑定。
Binder:在MVVM
中,声明式的数据和命令绑定是一个隐含的约定,它可以让开发者非常方便地实现view
和viewModel
的同步,避免编写大量繁杂的样板化代码。在MVVM
实现中,利用 ReactiveCocoa 来在view
和viewModel
之间充当binder
的角色,优雅地实现两者之间的数据绑定(同步)。
- MVVM与MVC联系
-
职责划分
MVVM若按照职责来划分的话,其根据首字母缩写如同view-model
术语一样, 对如何使用它们进行 iOS 开发体现得有点不太准确。
根据MVC
和MVVM
的职责划分,我们利用图解来表示,首先我们颠倒了MVC
中的V
和C
,于是首字母缩写更能准确地反映出实际开发中组件间的关系方位,给我们带来MCV
。若对MVVM
这么干, 将V(iew)
移到VM
的右边最终成为了MVMV
。很明显,这就是我们实际开发中一贯作风(套路)。
- 视图遵循区块尺寸大致可以理解成对应它们负责的工作量。
- 请注意到
MVC
中视图控制器(C
)有多大,(PS:意料之中?)。 - 可以看到我们巨大的视图控制器和
view-model
之间有大块工作上的重合。 - 也可以看看视图控制器在
MVVM
中的足迹有多大一部分是跟视图重合的。
ViewModel的职责
viewModel
一词的确不能充分表达其职责,无法顾名思义。很多小伙伴初次接触MVVM
设计模式时,都会卡在VM(视图模型)
的职责理解和角色定位,以及View = View+Controller
的理解上,Why?!!
。View Coordinator(视图协调者)
可能更好的表达viewModel
的意图。viewModel
从必要的资源(数据库,网络请求等)中获取原始数据,根据视图的展示逻辑,并处理成view (controller)
的展示数据。它(通常通过属性)暴露给视图控制器需要知道的仅关于显示视图工作的信息(理想地你不会暴漏你的data-model
对象)。-
ViewController的职责
如果抛开ViewController
不谈,突然发现这样的ViewModel
、Mode
以及View
不就是"MVC"
,一个以ViewModel
为中心的MVC
!!!这时,大家可能异口同声说:Are you fucking kidding me?!
。
这种理解完全是错误的!核心问题就在于对ViewModel
角色的定位不清!基于MVVM
设计思路,ViewModel
存在的目的在于抽离ViewController
中展示业务逻辑(PS:也就是上图MVC
中视图控制器(C
)和MVVM
中的VM
的重合部分),而不是替代ViewController
。既然不负责视图操作逻辑,ViewModel
中就不应该存在任何View对象
,更不应该存在Push/Present
等视图跳转逻辑。
其实MVVM
是一定需要Controller
的参与的,虽然MVVM
在一定程度上弱化了Controller
的存在感,并且给Controller
做了减负瘦身(PS:这难道不就是MVVM
的主要目的)。我们实际上最终以MVMCV
告终。Model View-Model Controller View
`MVVM`的正确打开方式如下:
![MVMCV.png](http://upload-images.jianshu.io/upload_images/1874977-83316d550a75ca16.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
从上图可知,`Controller`夹在`View`和`ViewModel`之间做的其中一个主要事情就是将`View`和`ViewModel`进行绑定。在逻辑上,`Controller`知道应当展示哪个`View`,`Controller`也知道应当使用哪个`ViewModel`来提供数据,然而`View`和`ViewModel`它们之间是互相不知道的,所以Controller仅关注于用 `view-model 的数据配置`和`管理各种各样的视图`。
所以Controller
在MVVM
中,一方面负责View
和ViewModel
之间的绑定,另一方面也负责常规的UI逻辑处理
。(PS:豁然开朗了没?柳暗花明了没?Six Six Six...)
-
MVVM模块层级图
三、MVVM Without ReactiveCocoa功能实践的前期准备
Talk is cheap,Show me the code。光说不练假把式,光练不说啥把式
。使用 MVVM
搭配 ReactiveCocoa
会很优雅地实现View
和ViewModel
之间的数据绑定,不过它的问题在于学习成本和维护成本比较高,但是切记:MVVM
的关键是要有ViewModel
!而不是 ReactiveCocoa。
RAC
是基于 KVO
构建的。所以也可以用 KVO
来让View
获取 ViewModel
的变化。但我们都知道 KVO
的槽点比较多,比如使用KVO
时,既需要进行 注册成为某个对象属性的观察者
,还要在合适的时间点将自己移除
,再加上需要 覆写一个又臭又长的方法 ,并在方法里 判断这次是不是自己要观测的属性发生了变化
等。这里可以使用 Facebook
开源的 KVOController,它比较优雅地处理了 KVO
存在的一些问题,同时又能发挥 KVO
带来的便捷性。
这也是笔者今天要讲的主题:如何不借助 ReactiveCocoa
来实现 MVVM
。Let's Do It。请注意,以下内容只是笔者针对使用MVVM Without ReactiveCocoa
在实践过程的心得体会以及细节处理,主要侧重分析 MVVM Without ReactiveCocoa
的实践思路和逻辑处理,详细设计还请参考源码。 当然我也会陈述我的观点来论证,但愿能唤起大家的共鸣,共同进步。(PS:这个Demo
就是笔者目前所负责项目的冰山一角,当然欢迎大家踊跃前往AppStore下载 小闲肉-母婴二手闲置购物平台,仅供参考。)
- UI效果图
登录效果图 | 首页效果图 |
---|---|
- 需求分析表
用户登录需求 | 商品首页需求 |
---|---|
只有用户输入了手机号和验证码,登录按钮才可点击 | 界面滚动流畅,纵享丝滑 |
用户输入的手机号必须是真实有效的 | 导航栏的样式根据用户的滚动而变化 |
验证码为四位有效数字 | 点击右下角的卡通头像,滚动顶部 |
当用户输入手机号码时需要从本地获取用户头像 | 响应商品界面上的事件处理,如商品、用户头像、地理位置、留言和点赞的事件处理 |
备注:右上角的填充 按钮,仅仅是减少开发者的输入(笔者的需求) |
备注:点击顶部搜索框,回退到列表页(笔者的需求) |
- 效果图
四、MVVM Without ReactiveCocoa的登录界面的实践
- 逻辑分析图
- ViewModel的设计
/// 登录界面的视图模型 -- VM
@interface SULoginViewModel1 : NSObject
/// 手机号
@property (nonatomic, readwrite, copy) NSString *mobilePhone;
/// 验证码
@property (nonatomic, readwrite, copy) NSString *verifyCode;
/// 登录按钮的点击状态
@property (nonatomic, readonly, assign) BOOL validLogin;
/// 用户头像
@property (nonatomic, readonly, copy) NSString *avatarUrlString;
/// 用户登录 为了减少View对viewModel的状态的监听 这里采用block回调来减少状态的处理
- (void)loginSuccess:(void(^)(id json))success
failure:(void (^)(NSError *error))failure;
@end
很明显viewModel
仅仅只暴漏了视图控制器所必需的最小量的信息,设置readonly
属性很有必要,同时,视图控制器C
实际上并不在乎 viewModel
是如何获得这些信息的。切记:ViewModel
千万不要主动对视图控制器C
以任何形式直接起作用或直接通告其变化,而是等待视图控制器C
来主动获取。
想必大家可能对下面的代码存在疑惑,原因可能是:不是说好的 View
绑定ViewModel
的呢?绑定呢?监听呢?....
/// 用户登录 为了减少View对viewModel的状态的监听 这里采用block回调来减少状态的处理
- (void)loginSuccess:(void(^)(id json))success
failure:(void (^)(NSError *error))failure;
对方不想和笔者说话并向笔者扔了一个API
设计
/// 是否正在执行
@property (nonatomic, readonly, assign) BOOL executing;
/// 请求失败的信息
@property (nonatomic, readonly, strong) NSError *error;
/// 请求成功的数据
@property (nonatomic, readonly, strong) id responseObject;
/// 调起登录
- (void) login;
这样设计其实也合理的,ViewController
的登录
按钮被点击时,调用viewModel
上的login
方法,同时ViewController
通过KVO
的方法监听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);
/// 根据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);
/// 成功的数据处理
}];
/// 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);
/// 失败的数据处理
}];
笔者不想和你说话并向你扔了一个问题思考。上面👆一个登陆(login
)操作,我们就要编写这么多代码,试想如果再多一个操作呢?再多两个操作呢?.... 如果不用block
回调,不管你们会不会,总之,我会。下面👇再看看利用block
的回调实现,你们就会解惑,释怀了,起码好受点。
[MBProgressHUD mh_showProgressHUD:@"Loading..."];
@weakify(self);
[self.viewModel loginSuccess:^(id json) {
@strongify(self);
[MBProgressHUD mh_hideHUD];
/// 成功的数据处理
} failure:^(NSError *error) {
/// 失败的数据处理
}];
-
ViewController(视图控制器)
- 视图控制器从
viewModel
获取的数据将用来:
- 当
validLogin
的值发生变化时,触发登录按钮
的enabled
的属性。 - 监听
avatarUrlString
的变化,来更新视图控制器的头像
的UIImageView
。
- 视图控制器对
viewModel
起如下作用:
- 每当
UITextField
中的文本发生变化, 更新viewModel
上的readwrite
属性mobilePhone
或者verifyCode
- 登录按钮被点击时,调用
viewModel
上的loginSuccess:failure
方法。
- 视图控制器不要做的事
- 发起登录的网络请求
- 判定登录按钮的有效性
- 来获取头像的地址(PS:有可能从本地数据库获取,也有可能通过网络请求来获取)
- ...
请再次注意视图控制器总的责任是处理
viewModel
中的变化。 - 视图控制器从
五、MVVM Without ReactiveCocoa的商品首页界面的实践
- ViewModel的设计
/// 商品首页的视图模型 -- 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;
/**
* 加载网络数据 通过block回调减轻view 对 viewModel 的状态的监听
@param success 成功的回调
@param failure 失败的回调
@param configFooter 底部刷新控件的状态 lastPage = YES ,底部刷新控件hidden,反之,show
*/
- (void)loadData:(void(^)(id json))success
failure:(void(^)(NSError *error))failure
configFooter:(void(^)(BOOL isLastPage))configFooter;
@end
-
ViewController(视图控制器)
视图控制器
通过调用viewModel
的loadBannerData:failure:
和loadData:failure:configFooter:
来获取商品首页的广告数据(SUBanner)
以及商品数据(SUGoods)
。视图控制器
通过使用viewModel
上的banners
和dataSource
数组中的对象来配置表格视图(tableView
)的tableViewHeader
和cell
。通常我们会期待展现dataSource
的是数据-模型
对象。同时你可能已经对其感到奇怪, 因为我们试图通过MVVM
模式不暴漏数据-模型
对象。 (前面提到过的)。
假设我们暴露数据-模型(SUGoods)
,那就分析如下:
我们不瞎,明显从上图👆可以看出视图 SUGoodsCell
直接引用了模型SUGoods
,这就有悖了MVVM
的初衷:** view和 view controller 都不能直接引用model,而是引用视图模型(viewModel) **
-
子ViewModel
我们必须明确:viewModel不必在屏幕上显示所有东西。在工作中如果遇到量级非常重的控制器,可以针对实际的业务,将一组业务逻辑相关的代码抽取到一个独立的视图模型中处理。你可用
子viewModel
来代表屏幕上更小的、更潜在的被封装的部分。
一般来说,viewController
可以带一个viewModel
,那如果出现Cell
时怎么办,Cell
里又包含了按钮,按钮又需要数据请求又怎么处理?这些都是比较常见的场景,也可以通过MVVM
来解决。
我们知道viewModel
的职责是为view
提供数据支持,Cell
也是一个View
,那么为Cell
配备一个viewModel
不就可以了么。所以相对于ViewController
的ViewModel
来说,Cell
上配备的viewModel
就是子viewModel
。
你不总是需要子viewModel
。 比如,笔者可能用表格tableHeaderView
视图来渲染简单的页面展示。它不是个可重用的组件,所以笔者可能仅将我们已经给视图控制器用过的相同的viewModel
传给那个自定义的header
视图。它会用到viewModel
中它需要的信息,而无视余下的部分。
针对上面👆发现的问题,笔者优化如下:
从上面👆可知,
dataSource
是一个里面装着SUGoodsItemViewModel
的对象数组,在表格视图中的 tableView: cellForRowAtIndexPath:
方法中,将会从视图控制器的viewModel
的dataSource
中通过正确的索引获取到子viewModel
, 并把它赋值给 cell
上的 viewModel
属性。
想必大家还有一个疑惑,数据-模型(SUGoods)是否要通过属性的方式暴露在子视图模型(SUGoodsItemViewModel)的.h文件中?
我们假设要通过SUGoodsItemViewModel
来提供给SUGoodsCell
展示下面👇的界面的数据:
商品模型(
SUGoods
)的数据结构如下:
/** 商品运费类型 */
typedef NS_ENUM(NSUInteger, SUGoodsExpressType) {
SUGoodsExpressTypeFree = 0, // 包邮
SUGoodsExpressTypeValue = 1, // 运费
SUGoodsExpressTypeFeeding = 2,// 待议
};
@interface SUGoods : SUModel
/// === 商品相关的属性 ===
....
/// === 商品中的用户相关的信息 ===
/// 用户ID
@property (nonatomic, readwrite, copy) NSString * userId;
/// 用户头像
@property (nonatomic, readwrite, copy) NSString * avatar;
/// 用户昵称:
@property (nonatomic, readwrite, copy) NSString * nickName;
/// 是否芝麻认证
@property (nonatomic, readwrite, assign) BOOL iszm;
@end
假设我们将数据-模型通过属性暴露
在子视图模型的.h中,笔者将设计SUGoodsItemViewModel.h/m
大致代码如下👇:
/// SUGoodsItemViewModel.h
/// 数据-模型(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;
}
笔者将设计SUGoodsCell.m
大致代码如下👇:
/// SUGoodsCell.m
- (void)bindViewModel:(SUGoodsItemViewModel *)viewModel
{
self.viewModel = viewModel;
/// 头像
[MHWebImageTool setImageWithURL:viewModel.goods.avatar placeholderImage:placeholderUserIcon() imageView:self.userHeadImageView];
/// 昵称
self.userNameLabel.text = viewModel.goods.nickName;
/// 芝麻认证
self.realNameIcon.hidden = !viewModel.goods.iszm;
/// 用户ID
self.userIdLabel.text = viewModel.userId;
}
假设我们将数据-模型不通过属性暴露
在子视图模型的.h中,笔者将设计SUGoodsItemViewModel.h/m
大致代码如下👇:
/// SUGoodsItemViewModel.h
/// 数据-模型(SUGoods)不暴露
@interface SUGoodsItemViewModel : NSObject
/// 用户头像
@property (nonatomic, readonly, copy) NSString * avatar;
/// 用户昵称:
@property (nonatomic, readonly, copy) NSString * nickName;
/// 是否芝麻认证
@property (nonatomic, readonly, assign) BOOL iszm;
/// 101921 PS:有时候需要通过user_id跳转到用户信息的界面
@property (nonatomic, readonly, copy) NSString * user_id;
/// 用户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;
/// 用户头像
@property (nonatomic, readwrite, copy) NSString * avatar;
/// 用户昵称:
@property (nonatomic, readwrite, copy) NSString * nickName;
/// 是否芝麻认证
@property (nonatomic, readwrite, assign) BOOL iszm;
@end
@implementation SUGoodsItemViewModel
- (instancetype)initWithGoods:(SUGoods *)goods
{
self = [super init];
if (self) {
self.goods = goods;
self.userId = [NSString stringWithFormat:@"用户ID:%@",goods.userId]
self.user_id = goods.userId;
self.nickName = goods.nickName;
self.avatar = goods.avatar;
self.iszm = goods.iszm;
}
return self;
}
笔者将设计SUGoodsCell.m
大致代码如下👇:
/// SUGoodsCell.m
- (void)bindViewModel:(SUGoodsItemViewModel *)viewModel
{
self.viewModel = viewModel;
/// 头像
[MHWebImageTool setImageWithURL:viewModel.avatar placeholderImage:placeholderUserIcon() imageView:self.userHeadImageView];
/// 昵称
self.userNameLabel.text = viewModel.nickName;
/// 芝麻认证
self.realNameIcon.hidden = !viewModel.iszm;
/// 用户ID
self.userIdLabel.text = viewModel.userId;
}
首先我们发现,如果不通过属性暴露数据模型,SUGoodsItemViewModel
跟SUGoods
也太想了吧,仅仅只是用readonly
代替readwirte
而已!为啥吃饱了事没饭干将其转化成 viewModel 的工作啊?神经病啊!!即使类似,viewModel
让我们限制信息只暴露给我们需要的地方, 提供额外数据转换的属性, 或为特定的视图计算数据。(此外,当可以不暴露可变数据-模型对象(SUGoods
)时也是极好的,因为我们希望 viewModel
自己承担起更新它们的任务,而不是靠视图或视图控制器。)
但是日常开发过程中笔者 强烈建议大家把数据模型(SUGoods
)暴露在子视图模型(SUGoodsItemViewModel
)的.h中。这样一来子视图模型的属性会相应的减少,大大减少了胶水代码
的产生。但是可能又会有人不想说话并向笔者抛了一个issue!!!
既然通过属性暴露了数据-模型(SUGoods)
了,为何还要暴露一个userId
的属性?有必要吗?很有必要!!!
上面已经提到过ViewModel
提供额外数据转换的属性, 或为特定的视图计算数据。显然我们完全可以不暴露userId
,仅仅只要我们在SUGoodsCell.m
中这样写即可,根本无伤大雅是吧。
/// SUGoodsCell.m
- (void)bindViewModel:(SUGoodsItemViewModel *)viewModel
{
self.viewModel = viewModel;
/// 头像
[MHWebImageTool setImageWithURL:viewModel.goods.avatar placeholderImage:placeholderUserIcon() imageView:self.userHeadImageView];
/// 昵称
self.userNameLabel.text = viewModel.goods.nickName;
/// 芝麻认证
self.realNameIcon.hidden = !viewModel.goods.iszm;
/// 用户ID
self.userIdLabel.text =[NSString stringWithFormat:@"用户ID:%@",viewModel.goods.userId] ;
}
对此,笔者只能微微一笑很倾城了。因为这个数据的属性过于简单,仅仅只是数据的拼接,看不出viewModel
的作用和强大。详情见下面👇商品运费Label
的显示逻辑:
/// 邮费情况
NSString *freightExplain = nil;
SUGoodsExpressType expressType = goods.expressType;
if (expressType==SUGoodsExpressTypeFree) {
// 包邮
freightExplain = @"包邮";
}else if(expressType == SUGoodsExpressTypeValue){
// 指定运费
NSString *extralFee = [NSString stringWithFormat:@"运费 ¥%@",goods.expressFee];
freightExplain = extralFee;
}else if (expressType == SUGoodsExpressTypeFeeding){
freightExplain = @"运费待议";
}
self.freightExplain = freightExplain;
至此,笔者相信大家都会把上面👆这段代码写在ViewModel
中,通过暴露一个只读(readonly)的freightExplain
属性供cell
获取展示,而不是Cell
中编写这段又臭又长的逻辑代码。
六、划重点,涨姿势
- 保证将
MVVM
中Model
设计成Thin-Model(瘦模型)
,避免其沦为Fat-Model(胖模型)
,且不要与ViewModel
混淆一谈,两者道不同,不相为谋
。 -
View
和ViewModel
之间存在数据和事件的双向绑定的关系,利用 ReactiveCocoa 来充当view
和viewModel
之间binder
的角色,优雅地实现两者之间的数据绑定(同步),切记:ReactiveCocoa 并非是实现MVVM
设计模式的充要条件。MVVM
的关键是要有ViewModel
!而不是 ReactiveCocoa -
MVVM
可以看成是MVMCV
的设计模式,从而引申出来Model
、ViewModel
、Controller
以及View
他们之间的角色定位,以及各自的职责所在。切勿试图萌生用ViewModel
来代替ViewController
,Controller
在MVVM
中负责View
和ViewModel
之间的绑定和常规的UI逻辑处理
,而ViewModel
目的在于抽离ViewController
中展示业务逻辑。ViewModel
和ViewController
在一起,但独立。 - 在
view/viewController
中不能直接引用模型Model
,viewModel
不必在屏幕上显示所有东西。针对实际的业务,将一组业务逻辑相关的代码抽取到一个独立的视图模型中处理(子ViewModel
)。 -
视图模型
可以通过属性的方式暴露一个只读
的数据模型
,视图模型负责提供额外数据转换的属性, 或为特定的视图提供计算数据。为了消除View
过多的观察ViewModel
的状态(属性)的变化,我们可以通过block
的方式回调请求数据。
七、代码阅读
由于这个功能笔者分别采用 MVC
和MVVM Without ReactiveCococa
来开发实践,毕竟萝卜白菜,各有所爱,目的就是便于大家更深层次的了解MVC
和MVVM
的异同,以及提供一个利用MVVM Without ReactiveCococa
真实开发的样例,希望能够打消大家对 MVVM
模式的顾虑。为了方便我们从宏观上了解功能的的整体结构,我们可以分别看看MVC
和MVVM Without RAC
的类图。大家可以跟着类图,顺藤摸瓜,秉承该看的看,不该看的偷偷看的原则,赶快行动起来吧。
-
MVC类图
-
MVVM Without RAC 类图
源码地址(PS: 还请star一下,不会怀孕🤰的)
MHDevelopExample_Objective_C 目录中的 MVC&MVVM文件夹中
八、期待
- 文章若对您有点帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议💗,切记学无止境。
- 针对文章所述内容,阅读期间任何疑问;请在文章底部批评指正,我会火速解决和修正问题。
- GitHub地址:https://github.com/CoderMikeHe
九、参考链接
- http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/ 👉 译文
- http://blog.leichunfeng.com/blog/2016/02/27/mvvm-with-reactivecocoa/
- https://github.com/leichunfeng/MVVMReactiveCocoa
- https://casatwy.com/iosying-yong-jia-gou-tan-viewceng-de-zu-zhi-he-diao-yong-fang-an.html
- http://www.cocoachina.com/ios/20160520/16004.html
- http://www.cocoachina.com/ios/20151020/13795.html
- http://draveness.me/kvocontroller.html