iOS开发精进iOSiOS

iOS 关于MVVM Without ReactiveCocoa

2017-06-18  本文已影响7377人  CoderMikeHe
一、概述
二、MVVM
  1. MVVM的基本概念
  1. MVVM与MVC联系
`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 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下载 小闲肉-母婴二手闲置购物平台,仅供参考。)

登录效果图 首页效果图
登录界面效果图一@2x.png 商品首页效果图一@2x.png
登录界面效果图二@2x.png 商品首页效果图二@2x.png
用户登录需求 商品首页需求
只有用户输入了手机号和验证码,登录按钮才可点击 界面滚动流畅,纵享丝滑
用户输入的手机号必须是真实有效的 导航栏的样式根据用户的滚动而变化
验证码为四位有效数字 点击右下角的卡通头像,滚动顶部
当用户输入手机号码时需要从本地获取用户头像 响应商品界面上的事件处理,如商品、用户头像、地理位置、留言和点赞的事件处理
备注:右上角的填充按钮,仅仅是减少开发者的输入(笔者的需求 备注:点击顶部搜索框,回退到列表页(笔者的需求
MVC和MVVM实践效果图.gif
四、MVVM Without ReactiveCocoa的登录界面的实践
登录界面逻辑图.png
/// 登录界面的视图模型 -- 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) {
   /// 失败的数据处理
}];
五、MVVM Without ReactiveCocoa的商品首页界面的实践
/// 商品首页的视图模型 -- 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
商品首页暴露数据模型.png

我们不瞎,明显从上图👆可以看出视图 SUGoodsCell直接引用了模型SUGoods,这就有悖了MVVM的初衷:** view和 view controller 都不能直接引用model,而是引用视图模型(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中编写这段又臭又长的逻辑代码。

六、划重点,涨姿势
七、代码阅读

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

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

猜你喜欢

热点阅读