美文网首页iOS开发精进iOSiOS
iOS 关于MVVM Without ReactiveCocoa

iOS 关于MVVM Without ReactiveCocoa

作者: CoderMikeHe | 来源:发表于2017-06-18 23:11 被阅读7377次
    一、概述
    • 通过上一篇文章的学习,我们对关于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:https://img.haomeiwen.com/i1874977/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
    九、参考链接

    相关文章

      网友评论

      • 十里桃花终是你:看你文章感觉你似乎是吧m上的功能移到了VM上,然后emmmmmm 解耦性和可重用性并不高,意义也不大的样子。
      • yeshenlong520:楼主 你的SUGoodsHeaderView.xib是怎么创建来的 我手动创建的都是那种无法拖动大小的view 求解
        yeshenlong520:@CoderMikeHe 好的,搞定了。
        CoderMikeHe:@yeshenlong520 或者你把那个xib,的属性的size 改成freedom即可
        CoderMikeHe:@yeshenlong520 应该是 选那个 EmptyView
      • 英俊神武:个人觉得啊,这个更像是MVCS,关于数据的双向绑定讲得太少了,应该把数据的双向绑定的过程,起因重点讲述一下的。
        CoderMikeHe:@我是宋仲基 双向绑定 还是得用 RAC,当然罗,数据的双向绑定 我也理解的一般。
      • 从小爱吃烤地瓜:楼主看到留言可以加下微信嘛,15201171327,有问题请教一下,在线等急~:pray:
      • woniu:收益甚多😆
        CoderMikeHe:@woniu 嗯嗯,感谢支持
      • 猎奇人:请问能不能给我发一份demo,GitHub上面的不能运行,谢谢
        猎奇人:@CoderMikeHe 哦,原来是在GitHub上,刚刚没看到,谢谢了
        CoderMikeHe:@猎奇人 不是有个百度云的地址吗?
      • tongxyj:可以,帮我解答了很多一直疑惑的问题。
        CoderMikeHe:@weakTong 对你开发有些许帮助就行。
      • 周英俊a:您好,我按照您这样的通过view model 来映射 控制器, 在logincontroller 中 viewmodel //@dynamic viewModel; 你这里的 loginView model 只有在appdelegate alloc 吗 ?
        CoderMikeHe:@像一棵树生活着 没太看明白你这段话。还请简信我哈。
      • 高思阳:谢谢分享。这里有个问题想问下,对于一个tableview的数据(itemArr)请求,应该放在哪里合适,model或者是controller还是viewModel呢?是不是还是放在controller里面比较好呢
        CoderMikeHe:@高思阳 ViewModel 比较合适。
      • F麦子:写的很耐心,看你的才算看懂了,其他的文章都是半知半解
      • F麦子:楼主,我想问一下,你们的App Store中的APP全是你一个开发的吗,哪个是你开发的啊
        CoderMikeHe:@X堇色 嗯嗯,我已经从上家公司了离职了。公司倒闭了,所以没人维护了。
        F麦子:@CoderMikeHe 小闲肉APP手机号登录不了啊,而且微信登陆会闪退
        CoderMikeHe:@X堇色 App开发的部分我也不清楚了,都是多人开发的。建议你还是关注技术,【偷笑】
      • 9bebf4d7642b:我pod install RAC git 那个链接下来的 为什么全是乱码啊 我目前是用ReactiveObjC
      • c14f24c98abe:感谢分享,大佬,我下您这个demo ReactiveCocoa 这个报错,怎么解决啊 用pod install 下载不下来 是不是 版本 的问题
        c14f24c98abe:@CoderMikeHe 昨天在家里 电脑 下下来 和今天 在公司 电脑下下来 结果是一样的
        c14f24c98abe:@CoderMikeHe 我就是Pod install 失败 然后 在百度网盘下载的 打开 跑不起来 ReactiveCocoa显示 这个报错
        CoderMikeHe:@时光会杀人 有一个百度分享的链接,下载下来直接跑即可。由于Podfile库文件过多,第一次Pod install 会比较慢。
      • 不辣先生:没太能理解那个数据的双向绑定,怎么保持数据同步的:joy:
        CoderMikeHe:@不辣先生 MVVM核心思想应该不是数据双向绑定吧,首先MVVM任务主要分担了大多数控制器的数据,通过使用RAC来实现数据绑定可能体现在:Cell上监听VM的的某个属性变化来更新子控件label的内容,如果是MVC,我们修改了模型的属性,cell上的label文字是不会改变的,我们需要刷新tableview才行,但是RAC的话,我们通过数据绑定,理论上,属性变化,则label的内容变化,无需刷新tableView,所以数据绑定只不过是MVVM隐含的一个组件罢了,用RAC无非就是优雅的实现数据绑定罢了。
        不辣先生:@CoderMikeHe 我看有些作者发表的mvvm实践,就有人评价没有体现mvvm的核心思想数据双向绑定,只是分离出了c的代码,与其说是mvvm不如说是mvp,我感觉你这个写得蛮好的就是还是没搞懂数据绑定:joy:
        CoderMikeHe:@不辣先生 其实双向绑定我也不是蛮理解。
      • Veer_Pan:pod引用五十几个库。。。。
        CoderMikeHe:@Veer_Pan 都是项目中开发比较常用的,没有屏蔽掉
      • 秋雨无痕:通俗易懂,好文
      • 47200923d724:楼主,能发一下你联系方式吗? 有些地方不是很了解,但不截图又不好表达:pray:
      • 柯丕安德柯丕:楼主 你好,你的Demo还是无法运行,能给份能运行的吗?拜谢。
        1299824045@qq.com
        CoderMikeHe:@黑化的小猪 好的,没问题,能否发给我你为什么运行出错的原因??我看看能否解决。
      • c737a410172c:樓主 好文筆,尤其demo總算我找了幾十篇文章,就你的可以跑起來,太感動了!
        CoderMikeHe:@GIGO_893d 这个。。好戏还在后头。看完再来回复哈。
      • doudo:首先谢谢作者的分享,很细心。
        不过有些疑问,还想请教下:
        1.mvvm中很重要的一个地方,就是双向绑定,作者没有着重写,也是一直困惑我的地方。我先说一下我的理解吧。双向绑定我理解的是view和viewModel之间的双向,viewModel通过kvo等机制把更新通知给view,view通过用户action操作来通过viewModel。是这样的吧。
        2.第二个疑问是model和viewModel之间的关系,比如view通过点击事件改编了viewModel的一个属性值,此时viewModel通过什么方式去改变model呢,虽然viewModel拥有model。通过viewModel对应属性的set方法吗?
        希望帮忙解答一下,多谢:pray:
        CoderMikeHe:@doudo //View
        - (void)buttonTapped
        {
        viewModel.name = @"newValue";
        viewModel.model.name = @"newValue";
        }
        一般viewModel暴露的都是处理过model。name,例如拼接,如果viewModel.name 跟model.name一样,你完全没必要在viewModel暴露一个name了。你取值的时候直接取viewModel.model.name 即可。viewModel暴露的属性 一般都是经过处理后得到的数据。
        doudo://ViewModel
        @interface ViewModel:NSObject
        @property (.,readonly) Model *model;
        @property(..) NSString *name;
        @EnD

        @Implementation ViewModel
        - (void)setName:(NSString *)name
        {
        _name = name;
        _model.name=name;
        }
        @EnD

        //View
        - (void)buttonTapped
        {
        viewModel.name = @"newValue";
        }

        此时viewmodel的值变了,model如何做相应的改变呢?你说viewmodel通过属性暴露model,是怎么操作的呢?
        CoderMikeHe:@doudo 1.基本是这样的逻辑,View监听ViewModel提供的属性(内部其实是Model的属性变化)的变化,从而来更新自身的UI;View通过UserAction来操作ViewModel的属性,其内部主要是更新Model的属性。从而可想而知,ViewModel充当了桥梁作用。
        2. 可以这样写,当然我喜欢在ViewModel的.h文件暴露一个readonly的模型,直接操纵模型即可。
      • 新月如火:最近看了一些文章,还在学习中~部分文章中ViewModel是偏向Model的,即一个ViewModel对应一个Model。而楼主的文中ViewModel是偏向Controller的,一个Controller对应一个ViewModel(不太复杂的情况)。不知这两种设计基于什么考虑的?又有何区别?还请大佬有空的时候解解惑。
        CoderMikeHe:@新月如火 ViewModel其实大多数情况都是用来处理逻辑的,当然我们应该把之前MVC的Model层逻辑放到ViewModel,也将Controller的一些业务逻辑放到ViewModel中,然后Controller只需要关心ViewModel提供的数据即可,其实也没有什么偏向性,关键是用来充当View(View+Controller)和Model的桥梁罢了。
      • Harely:大神!满满的干货,超崇拜!对着代码看的博客,写的很好!可以加一下微信吗?能不能加一个微信好友!
      • LoveY34:MVVM是需要针对视图的每一层级的子视图创建viewModel吗?
        CoderMikeHe:@LoveY34 主要是看视图的复杂性,来确定是否为其增加一个视图模型,灵活应用咯
      • 阿凡提说AI:demo运行出错怎能解决?
        diff: /Podfile.lock: No such file or directory
        diff: /Manifest.lock: No such file or directory
        error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
        CoderMikeHe:@张璠 你看更新一下 Cocopods 试试。
        阿凡提说AI:@CoderMikeHe 试了,不管用
        CoderMikeHe:@张璠 pod install
      • ClearWB:楼主大大好文笔,对MVVM又有了新的理解和认识,谢谢!PS,我下载你们的"小闲肉"app,我用微信三方登录的时候,会闪退。不知道是不是我手机系统的事情,我是iOS9.2系统。
        CoderMikeHe:@ClearWB 好的,谢谢反馈哈。
      • 红街咖啡:KVO监听的步骤写的不清楚,这个框架怎么监听使用啊,我看都没有头绪
        CoderMikeHe:@红街咖啡 我的参考链接已经指出:https://draveness.me/kvocontroller
      • 红街咖啡:ViewModel中的loginSuccess方法根本就不会执行
        CoderMikeHe:loginSuccess 是按钮点击事件触发的
        CoderMikeHe:@红街咖啡 好的,我等下看看哈。
      • 售前界的不死小强:层层嵌套真的好吗
        红街咖啡:ViewModel中的loginSuccess方法根本就不会执行
        售前界的不死小强:@CoderMikeHe 有微信或者QQ吗,有些问题想请教下的。
        CoderMikeHe:@狼牙特战026_西伯利亚狼 项目开发基类的存在,在所难免的。
      • CNMD_LJ:写的不错,但是严格来说直接把数据模型暴露给cell肯定不行的,这样model和view直接联系耦合在一起了,如果要更换model还需要修改view层,view层不应该和业务数据有耦合,不过一般简单的模块这么简单做没什么太大的问题,不为了分层而分层。如果不暴露数据模型,只暴露一些只读的显示属性在viewModel的.h文件中,如果显示数据很多的话都在里面太臃肿了,很多情况数据模型到显示还需要一些计算和转换合成的过程。这个业务数据model到视图数据model的过程我觉得可以做一些工作
        CoderMikeHe:@Mr_金 是的。view层是不应该和业务数据有耦合,只是负责配置控件属性和显示数据,计算的逻辑或者合成这些操作,可以放到视图模型(viewModel)里面去处理。如果cell显示数据很多的话,但是需要计算或者合成这些操作应该不多的,我们只需要把这部分逻辑处理即可,其他的完全可以通过`viewModel.model.xxx`这种来设置。
      • 黄鱼儿啦啦啦:发现demo 里面 RAC那块 loading不消息,push 不过去,有这bug
        CoderMikeHe:@黄鱼儿啦啦啦 能否给出死循环的具体代码位置。
        黄鱼儿啦啦啦:@CoderMikeHe Goods页面死循环了
        CoderMikeHe:@黄鱼儿啦啦啦 好的啦,你先不要显示 loading ,我去检查检查
      • LoveY34:楼主!你好!如果子ViewModel里面把model当做属性暴露出来的话,View不是又直接Model接触了!?子ViewModel只是对Model的封装吗?为了制造thin-Model?
        CoderMikeHe:@LoveY34 差不多吧,主要是抽离控制器或者View的业务逻辑到vviewModel中去。
        LoveY34:@CoderMikeHe MVVM设计模式我没用过,MVVM模式中一般都是这么做的嘛?利用ViewModel在View和Model之间做中介实现数据的传递?
        CoderMikeHe:@LoveY34 其实这里我也平常使用比较纠结的地方,该不该把model暴露在子viewModel的属性上,但是你如果只是单纯的把model当做子ViewModel的一个属性来看,其实是可以理解的。子viewModel不仅仅只是对Model的封装,通常我们是可以用来处理一些业务逻辑的,但是我比较喜欢把一些大的逻辑 ,在主viewModel中处理,这样逻辑处理,较为集中,不显得比较混乱。
      • Johnny_Chang:写的很牛逼,受益匪浅
      • Simple_Dev:看了很多关于MVVM的文章,很多都是一个理论,讲思想,你这个最实在了,写的很好。只是你的demo下载下来,我一直pod install失败,pod update repo也不行。能不能发一份代码到我的邮箱,实在是困于MVVM太久了。12479697@qq.com。不胜感激啊!
        Simple_Dev:说实话,看了代码,真的有点头晕。子类的层层嵌套。但是看的我觉得,自己之前写的代码真的太渣了,有很多巧妙之处,赞。头晕,然我去休息一会,继续看。
        Simple_Dev:@CoderMikeHe 嗯,好的。感谢,万分感谢。好好研究一哈去!谢谢!:pray:
        CoderMikeHe:可以可以,我直接给你百度云链接,注意查收。如果崩溃,请关闭全局断点。这个是FBMemoryProfiler的锅,去掉全局断点即可。
      • zombie:写的很好 最近项目要用帮助很大
        CoderMikeHe:可以可以 ,有帮助就行。
      • 小李广17:虽然不认识,但是从文采能感觉是位帅哥:smiley:
      • LittleYuz:赞,写的很详细,对MVVM有了初步的理解,果然还是实际的代码比较容易理解
        CoderMikeHe:@奋斗的小黄鸟 不客气,对你有用才是我的初衷。
        奋斗的小黄鸟:谢谢啦 挺不错的 赞一个
        CoderMikeHe:@K24LFY 是的,之前我也是苦于没有代码参照,导致使用MVVM不是很得心应手。

      本文标题:iOS 关于MVVM Without ReactiveCocoa

      本文链接:https://www.haomeiwen.com/subject/sjlzfxtx.html