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

一、概述
  • 通过上一篇文章的学习,我们对关于MVC的弊端的产生和MVVMviewModel的职责及其使用注意事项,想必都有了些许了解和认识,最起码What is MVC ? What is MVVM ?,大家也不会感觉这是最熟悉的陌生人了吧。笔者不才,本文将着重谈谈MVVM在iOS开发中的实际运用,以及自身通过实践探索出来的经验之谈,同时希望能让大家更加深刻体会到MVVMMVVM各自的职责,以及VVM之间那份剪不断,理还乱的缠绵往事。
  • 本文只是笔者在实践MVVM过程中的些许见解,在此抛砖引玉,共同探讨下 MVVM 的实践思路,希望能够打消你对 MVVM 模式的顾虑 ,提供一点思路,少走一些弯路,填补一些细坑。文章仅供大家参考,若有不妥之处,还望不吝赐教,欢迎批评指正。
  • MVVM基础知识以及其使用注意不了解的,请务必戳我👉 iOS 关于MVC和MVVM设计模式的那些事
二、MVVM
  1. MVVM的基本概念
  • MVVM的结构图


    MVVM结构图.png
  • MVVM的定义
    从上图中,我们可以非常清楚地看到 MVVM 中四个组件之间的关系。注:除了 viewviewModelmodel 之外,MVVM 中还有一个非常重要的隐含组件 binder
    Model :MVC中的model保持一致,完全取决于你的"偏好设置"。你可能会为model封装一些额外的操作数据的业务逻辑,虽然苹果是推崇你这么干的,但是笔者认为不妥,这样很可能会导致一个胖Model的产生,而且胖Model相对比较难移植胖Model随着产品的迭代会更加的Fat,最终难以维护,一胖毁所有。我更倾向于把它当做一个容纳表现数据-模型(data-model)对象信息的结构体(瘦Model),并通过一个单独的管理类来维护/创建/管理模型的统一逻辑,又或者可以通过使用Category来扩充业务逻辑。MVVM是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:ModelViewModel(PS:感觉是否有点道理)。
    View:MVC 中的viewcontroller 组成,负责 UI 的展示,绑定 viewModel中的属性,触发 viewModel 中的命令以及呈现由viewModel提供的数据。
    View-Model: 千万不要把它与传统数据-模型结构中模型混为一谈。 它的职责之一就是作为一个表现视图显示自身所需数据的静态模型;但它也有收集, 解释和转换那些数据的责任。它是从 MVCcontroller 中抽取出来的展示逻辑,负责从 model中获取 view 所需的数据,转换成 view可以展示的数据,并暴露公开的属性和命令供 view 进行绑定。
    Binder:MVVM 中,声明式的数据和命令绑定是一个隐含的约定,它可以让开发者非常方便地实现 viewviewModel的同步,避免编写大量繁杂的样板化代码。在MVVM实现中,利用 ReactiveCocoa 来在viewviewModel 之间充当 binder 的角色,优雅地实现两者之间的数据绑定(同步)。
  1. MVVM与MVC联系
  • 职责划分
    MVVM若按照职责来划分的话,其根据首字母缩写如同 view-model术语一样, 对如何使用它们进行 iOS 开发体现得有点不太准确。
    根据MVCMVVM的职责划分,我们利用图解来表示,首先我们颠倒了 MVC 中的 VC,于是首字母缩写更能准确地反映出实际开发中组件间的关系方位,给我们带来MCV。若对MVVM这么干, 将V(iew)移到VM的右边最终成为了 MVMV。很明显,这就是我们实际开发中一贯作风(套路)。

    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不谈,突然发现这样的ViewModelMode以及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

    Controller的职责.gif

`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 的数据配置`和`管理各种各样的视图`。

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

  • MVVM模块层级图


    模块层级图.png
三、MVVM Without ReactiveCocoa功能实践的前期准备

Talk is cheap,Show me the code。光说不练假把式,光练不说啥把式。使用 MVVM 搭配 ReactiveCocoa会很优雅地实现ViewViewModel之间的数据绑定,不过它的问题在于学习成本和维护成本比较高,但是切记: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效果图
登录效果图 首页效果图
登录界面效果图一@2x.png
商品首页效果图一@2x.png
登录界面效果图二@2x.png
商品首页效果图二@2x.png
  • 需求分析表
用户登录需求 商品首页需求
只有用户输入了手机号和验证码,登录按钮才可点击 界面滚动流畅,纵享丝滑
用户输入的手机号必须是真实有效的 导航栏的样式根据用户的滚动而变化
验证码为四位有效数字 点击右下角的卡通头像,滚动顶部
当用户输入手机号码时需要从本地获取用户头像 响应商品界面上的事件处理,如商品、用户头像、地理位置、留言和点赞的事件处理
备注:右上角的填充按钮,仅仅是减少开发者的输入(笔者的需求 备注:点击顶部搜索框,回退到列表页(笔者的需求
  • 效果图
MVC和MVVM实践效果图.gif
四、MVVM Without ReactiveCocoa的登录界面的实践
  • 逻辑分析图
登录界面逻辑图.png
  • 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的方法监听executingerrorresponseObject的属性即可,代码大致如下:

_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(视图控制器)

    1. 视图控制器从 viewModel获取的数据将用来:
    • validLogin的值发生变化时,触发登录按钮enabled的属性。
    • 监听avatarUrlString的变化,来更新视图控制器的头像UIImageView
    1. 视图控制器对 viewModel 起如下作用:
    • 每当 UITextField 中的文本发生变化, 更新 viewModel上的 readwrite属性 mobilePhone或者verifyCode
    • 登录按钮被点击时,调用viewModel上的loginSuccess:failure方法。
    1. 视图控制器不要做的事
    • 发起登录的网络请求
    • 判定登录按钮的有效性
    • 来获取头像的地址(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(视图控制器)

    视图控制器通过调用viewModelloadBannerData:failure:loadData:failure:configFooter:来获取商品首页的广告数据(SUBanner)以及商品数据(SUGoods)视图控制器通过使用viewModel上的bannersdataSource数组中的对象来配置表格视图(tableView)的tableViewHeadercell。通常我们会期待展现 dataSource 的是数据-模型对象。同时你可能已经对其感到奇怪, 因为我们试图通过 MVVM模式不暴漏数据-模型对象。 (前面提到过的)。
    假设我们暴露数据-模型(SUGoods),那就分析如下:

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

我们不瞎,明显从上图👆可以看出视图 SUGoodsCell直接引用了模型SUGoods,这就有悖了MVVM的初衷:** view和 view controller 都不能直接引用model,而是引用视图模型(viewModel) **

  • 子ViewModel

    我们必须明确:viewModel不必在屏幕上显示所有东西。在工作中如果遇到量级非常重的控制器,可以针对实际的业务,将一组业务逻辑相关的代码抽取到一个独立的视图模型中处理。你可用子viewModel 来代表屏幕上更小的、更潜在的被封装的部分。
    一般来说,viewController可以带一个 viewModel,那如果出现 Cell时怎么办,Cell里又包含了按钮,按钮又需要数据请求又怎么处理?这些都是比较常见的场景,也可以通过 MVVM 来解决。
    我们知道 viewModel 的职责是为 view 提供数据支持,Cell 也是一个 View,那么为 Cell配备一个viewModel 不就可以了么。所以相对于ViewControllerViewModel来说,Cell上配备的viewModel就是子viewModel
    你不总是需要 子viewModel。 比如,笔者可能用表格 tableHeaderView 视图来渲染简单的页面展示。它不是个可重用的组件,所以笔者可能仅将我们已经给视图控制器用过的相同的 viewModel传给那个自定义的 header 视图。它会用到 viewModel中它需要的信息,而无视余下的部分。
    针对上面👆发现的问题,笔者优化如下:

商品首页子视图.png

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

想必大家还有一个疑惑,数据-模型(SUGoods)是否要通过属性的方式暴露在子视图模型(SUGoodsItemViewModel)的.h文件中?
我们假设要通过SUGoodsItemViewModel来提供给SUGoodsCell展示下面👇的界面的数据:

商品的用户信息.png

商品模型(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;
 }

首先我们发现,如果不通过属性暴露数据模型,SUGoodsItemViewModelSUGoods也太想了吧,仅仅只是用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中编写这段又臭又长的逻辑代码。

六、划重点,涨姿势
  • 保证将MVVMModel设计成Thin-Model(瘦模型),避免其沦为Fat-Model(胖模型),且不要与ViewModel混淆一谈,两者道不同,不相为谋
  • ViewViewModel之间存在数据和事件的双向绑定的关系,利用 ReactiveCocoa 来充当viewviewModel 之间 binder 的角色,优雅地实现两者之间的数据绑定(同步),切记:ReactiveCocoa 并非是实现MVVM设计模式的充要条件。MVVM的关键是要有ViewModel!而不是 ReactiveCocoa
  • MVVM可以看成是MVMCV的设计模式,从而引申出来ModelViewModelController以及View他们之间的角色定位,以及各自的职责所在。切勿试图萌生用ViewModel来代替ViewControllerControllerMVVM中负责ViewViewModel之间的绑定和常规的UI逻辑处理,而ViewModel目的在于抽离ViewController中展示业务逻辑。ViewModelViewController在一起,但独立。
  • view/viewController 中不能直接引用模型ModelviewModel 不必在屏幕上显示所有东西。针对实际的业务,将一组业务逻辑相关的代码抽取到一个独立的视图模型中处理(子ViewModel)。
  • 视图模型可以通过属性的方式暴露一个只读数据模型,视图模型负责提供额外数据转换的属性, 或为特定的视图提供计算数据。为了消除View过多的观察ViewModel的状态(属性)的变化,我们可以通过block的方式回调请求数据。
七、代码阅读

由于这个功能笔者分别采用 MVCMVVM Without ReactiveCococa来开发实践,毕竟萝卜白菜,各有所爱,目的就是便于大家更深层次的了解MVCMVVM的异同,以及提供一个利用MVVM Without ReactiveCococa真实开发的样例,希望能够打消大家对 MVVM 模式的顾虑。为了方便我们从宏观上了解功能的的整体结构,我们可以分别看看MVCMVVM Without RAC的类图。大家可以跟着类图,顺藤摸瓜,秉承该看的看,不该看的偷偷看的原则,赶快行动起来吧。

  • MVC类图


    MVC类图.png
八、期待
  1. 文章若对您有点帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议💗,切记学无止境。
  2. 针对文章所述内容,阅读期间任何疑问;请在文章底部批评指正,我会火速解决和修正问题。
  3. GitHub地址:https://github.com/CoderMikeHe
九、参考链接

推荐阅读更多精彩内容