iOS 关于MVVM Without ReactiveCocoa设计模式的那些事

一、概述

通过上一篇文章的学习,我们对关于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

1.MVVM的基本概念

  • MVVM的结构图

1874977-9bab0f07280d0134.png

MVVM结构图.png

  • 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 的角色,优雅地实现两者之间的数据绑定(同步)。

2.MVVM与MVC联系

  • 职责划分

MVVM若按照职责来划分的话,其根据首字母缩写如同 view-model术语一样, 对如何使用它们进行 iOS 开发体现得有点不太准确。

根据MVC和MVVM的职责划分,我们利用图解来表示,首先我们颠倒了 MVC 中的 V 和 C,于是首字母缩写更能准确地反映出实际开发中组件间的关系方位,给我们带来MCV。若对MVVM这么干, 将V(iew)移到VM的右边最终成为了 MVMV。很明显,这就是我们实际开发中一贯作风(套路)。

1874977-437efcdc506ab369.png

MVC&MVVM.png

  • 视图遵循区块尺寸大致可以理解成对应它们负责的工作量。

  • 请注意到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

1874977-64a31221bb6b8b6e.gif

Controller的职责.gif

MVVM的正确打开方式如下:

1874977-83316d550a75ca16.png

MVMCV.png

从上图可知,Controller夹在View和ViewModel之间做的其中一个主要事情就是将View和ViewModel进行绑定。在逻辑上,Controller知道应当展示哪个View,Controller也知道应当使用哪个ViewModel来提供数据,然而View和ViewModel它们之间是互相不知道的,所以Controller仅关注于用 view-model 的数据配置和管理各种各样的视图。

所以Controller在MVVM中,一方面负责View和ViewModel之间的绑定,另一方面也负责常规的UI逻辑处理。(PS:豁然开朗了没?柳暗花明了没?Six Six Six...)

  • MVVM模块层级图

1874977-f8c57cfd22113349.png

模块层级图.png

三、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效果图

1874977-43799098b176720f.png

登录界面效果图一@2x.png

1874977-7faedaac53853529.png

商品首页效果图一@2x.png

1.png

登录界面效果图二@2x.png

1874977-6ffeafb585eaef00.png

商品首页效果图二@2x.png

  • 需求分析表

QQ截图20170628134602.png

  • 效果图

5.gif

MVC和MVVM实践效果图.gif

四、MVVM Without ReactiveCocoa的登录界面的实践

  • 逻辑分析图

1874977-6a3b9021afbab5ea.png

登录界面逻辑图.png

  • ViewModel的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 登录界面的视图模型 -- 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的呢?绑定呢?监听呢?....

1
2
3
/// 用户登录 为了减少View对viewModel的状态的监听 这里采用block回调来减少状态的处理
- (void)loginSuccess:(void(^)(id json))success
          failure:(void (^)(NSError *error))failure;

对方不想和笔者说话并向笔者扔了一个API设计

1
2
3
4
5
6
7
8
/// 是否正在执行
@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的属性即可,代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_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* _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* _Nonnull change) {
        @strongify(self);
         /// 成功的数据处理
}];
 
/// binding self.viewModel.error
[_KVOController mh_observe:self.viewModel keyPath:@ "error"  block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary* _Nonnull change) {
        @strongify(self);
         /// 失败的数据处理
}];

笔者不想和你说话并向你扔了一个问题思考。上面????一个登陆(login)操作,我们就要编写这么多代码,试想如果再多一个操作呢?再多两个操作呢?.... 如果不用block回调,不管你们会不会,总之,我会。下面????再看看利用block的回调实现,你们就会解惑,释怀了,起码好受点。

1
2
3
4
5
6
7
8
9
[MBProgressHUD mh_showProgressHUD:@ "Loading..." ];
@weakify(self);
[self.viewModel loginSuccess:^(id json) {
     @strongify(self);
     [MBProgressHUD mh_hideHUD];
     /// 成功的数据处理
} failure:^(NSError *error) {
    /// 失败的数据处理
}];
  • ViewController(视图控制器)

1.视图控制器从 viewModel获取的数据将用来:

  • 当validLogin的值发生变化时,触发登录按钮的enabled的属性。

  • 监听avatarUrlString的变化,来更新视图控制器的头像的UIImageView。

2.视图控制器对 viewModel 起如下作用:

  • 每当 UITextField 中的文本发生变化, 更新 viewModel上的 readwrite属性 mobilePhone或者verifyCode

  • 登录按钮被点击时,调用viewModel上的loginSuccess:failure方法。

3.视图控制器不要做的事

  • 发起登录的网络请求

  • 判定登录按钮的有效性

  • 来获取头像的地址(PS:有可能从本地数据库获取,也有可能通过网络请求来获取)

  • ...

请再次注意视图控制器总的责任是处理viewModel中的变化。

五、MVVM Without ReactiveCocoa的商品首页界面的实践

  • ViewModel的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// 商品首页的视图模型 -- VM
@interface SUGoodsViewModel1 : NSObject
/// banners
@property (nonatomic, readonly, copy) NSArray*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),那就分析如下:

1874977-c02a6dd2b8a5660b.png

商品首页暴露数据模型.png

我们不瞎,明显从上图????可以看出视图 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中它需要的信息,而无视余下的部分。

针对上面????发现的问题,笔者优化如下:

6.png

商品首页子视图.png


从上面????可知,dataSource是一个里面装着SUGoodsItemViewModel的对象数组,在表格视图中的 tableView: cellForRowAtIndexPath:方法中,将会从视图控制器的viewModel的dataSource中通过正确的索引获取到子viewModel, 并把它赋值给 cell上的 viewModel属性。

想必大家还有一个疑惑,数据-模型(SUGoods)是否要通过属性的方式暴露在子视图模型(SUGoodsItemViewModel)的.h文件中?

我们假设要通过SUGoodsItemViewModel来提供给SUGoodsCell展示下面????的界面的数据:

1874977-7568807b96c57134.png

商品的用户信息.png


商品模型(SUGoods)的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 商品运费类型 */
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大致代码如下????:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// 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大致代码如下????:

1
2
3
4
5
6
7
8
9
10
11
12
13
///  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大致代码如下????:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/// 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大致代码如下????:

1
2
3
4
5
6
7
8
9
10
11
12
13
/// 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中这样写即可,根本无伤大雅是吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
///  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的显示逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 邮费情况
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类图

1874977-77c70f828e000995.png

MVC类图.png

  • MVVM Without RAC 类图

1874977-cdfdebf13781070a.png

MVVMWithoutRAC类图.png

源码地址(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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值