前言
在开发App的时候,我们的基本目标一般有以下几点:
- `可靠性 - App的功能能够正常使用`
- `健壮性 - 在用户非正常使用的时候,app也能够正常反应,不要崩溃`
- `效率性 - 启动时间,耗电,流量,界面反应速度在用户容忍的范围以内`
上面三点是表象层的东西,是大多数开发者或者团队会着重注意的。除了这三点,还有一些目标是工程方面的也是开发者要注意的:
- `可修改性/可扩展性 - 软件需要迭代,功能不断完善`
- `容易理解 - 代码能够容易理解`
- `可测试性 - 代码能够方便的编写单元测试和集成测试`
- `可复用性 - 不用一次又一次造轮子`
基于这些设计目标和理念,软件设计领域又有了设计模式。MVC/MVVM都是就是设计模式的一种。
在MVC的架构中,Model持有数据,View显示与用户交互的界面,而ViewController调解Model和View之间的交互。
现在,MVC 依然是目前主流客户端编程框架,但同时它也被调侃成Massive View Controller(重量级视图控制器),
开发者在开发中无可避免被下面几个问题所困扰:
- 厚重的ViewController
- 遗失的网络逻辑(无立足之地)
- 较差的可测试性
而MVVM这种新的代码组织方式就可以解决这些问题,本文就MVVM的架构设计做个简单的个人总结。
MVVM概述
从图中我们可以看到MVVM的关系基本是:View <-> C <-> ViewModel <-> Model,
严格来说MVVM其实是MVCVM。Controller夹在View和ViewModel之间做的其中一个主要事情就是将View和ViewModel进行绑定. 在逻辑上,Controller知道应当展示哪个View,Controller也知道应当使用哪个ViewModel, 然而View和ViewModel它们之间是互相不知道的,所以Controller就负责控制他们的绑定关系。
MVVM 一种可以很好地解决Massive View Controller问题的办法
就是将 Controller 中的展示逻辑抽取出来,放置到一个专门的地方,
而这个地方就是 viewModel 。MVVM衍生于MVC,是对 MVC 的一种演进,
它促进了 UI 代码与业务逻辑的分离。
它正式规范了视图和控制器紧耦合的性质,并引入新的组件。他们之间的结构关系如下:
不难看出,MVVM是对MVC的扩展,所以MVVM可以完美的兼容MVC。 对于一个界面来说,有时候View和ViewModel往往不止一个,MVVM也可以组合使用:
MVVM 的基本概念
- 在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件,
Controller可以当作一个重量级的View(负责界面切换和处理各类系统事件)。
- view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel)
- viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方,
它的职责之一就是作为一个表现视图显示自身所需数据的静态模型;但它也有收集, 解释和转换那些数据的责任。
它是从 MVC 的 controller 中抽取出来的展示逻辑,负责从 model中获取 view 所需的数据,
转换成 view可以展示的数据,并暴露公开的属性和命令供 view 进行绑定。
- 使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性。
MVVM 的注意事项
- viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。
- viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。
一方面负责View和ViewModel之间的绑定,另一方面也负责常规的UI逻辑处理。
- view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,
任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)
- viewModel 引用model,但反过来不行
- viewModel 绝对不能包含视图 view(UIKit.h),不然就跟 view 产生了耦合,不方便复用和测试。
- viewModel之间可以有依赖。
- viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。
关于MVVM Without ReactiveCocoa
为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa,
KVO,Notification,block,delegate和target-action都可以用来做数据通信,
从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅,
使用函数响应式框架能更好的实现数据和视图的双向绑定(ViewModel的数据可以显示到View上,
View上的操作同样会引起ViewModel的变化),降低了ViewModel和View的耦合度。
如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM。
MVVM的关键是要有ViewModel。而不是ReactiveCocoa、RXSwift或RXJava等。 而在现实中我倾向于使用 block而不是 KVO,因为KVO的代码量太大了,block则简洁的多。
ReactiveCocoa或RXSwift通过这两个框架可以实现ViewModel和View的双向绑定,
但同样会存在几个比较重大的问题。 首先,ReactiveCocoa或RXSwift的学习成本很高;
其次,
数据绑定使得 Bug 很难被调试,当界面出现异常,可能是View的问题,也可能是数据ViewModel的问题。 而数据绑定会使一个位置的bug传递到其他位置,难以定位。
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
viewModelreadonly视图控制器CviewModelViewModel视图控制器C视图控制器CViewViewModel
/// 用户登录 为了减少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登录viewModelloginViewControllerKVOexecutingerrorresponseObject
_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);
/// 失败的数据处理
}];
loginblockblock
[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中的变化。
商品首页界面的实践
- 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
视图控制器viewModelloadBannerData:failure:loadData:failure:configFooter:广告数据(SUBanner)商品数据(SUGoods)视图控制器viewModelbannersdataSourcetableViewtableViewHeadercelldataSource数据-模型MVVM数据-模型数据-模型(SUGoods)
SUGoodsCellSUGoodsMVVM
子viewModelviewControllerviewModelCellCellMVVMviewModelviewCellViewCellviewModelViewControllerViewModelCellviewModel子viewModel子viewModeltableHeaderViewviewModelheaderviewModel
dataSourceSUGoodsItemViewModeltableView: cellForRowAtIndexPath:viewModeldataSource子viewModelcellviewModel
SUGoodsItemViewModelSUGoodsCell
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
通过属性暴露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;
}
既然通过属性暴露了数据-模型(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 的更瘦身的架构设计方式
MVVM的出现主要是为了解决在开发过程中Controller越来越庞大的问题,变得难以维护,
所以MVVM把数据加工的任务从Controller中解放了出来,使得Controller只需要专注于数据调配的工作,
ViewModel则去负责数据加工并通过通知机制让View响应ViewModel的改变。
MVVM是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:Model和ViewModel。
ViewModel本质上算是Model层(因为是胖Model里面分出来的一部分),所以 ViewModel里面不能包含任何 UIKit的内容。
而且View并不一定适合直接持有ViewModel,因为ViewModel有可能并不是只服务于特定的一个View,
如果我们对于单个复杂View设计一个 ViewModel 是可以让该 View 持有该 ViewModel的。
如图我们设计了一个基于 MVVM 的更瘦身的架构设,这个架构中:
* View - 用来呈现用户界面
* ViewManger - 用来处理View的常规事件,负责管理View
* Controller - 负责ViewManger和ViewModel之间的绑定,负责控制器本身的生命周期。
* ViewModel - 存放各种业务逻辑和网络请求,不能存在 UIKit 有关的东西。
* Model - 用来呈现数据
这种设计的目的是保持View和Model的高度纯洁,提高可扩展性和复用度。
在日常开发中,ViewModel是为了拆分Controller业务逻辑而存在的,
所以ViewModel需要提供公共的服务接口,以便为Controller提供数据。
而ViewManger的作用相当于一个小管家,帮助Controller来分别管理每个subView,ViewManger负责接管来自View的事件,
也负责接收来自Controller的模型数据,
而View进行自己所负责的视图数据绑定工作。
Controller则是最后的大家长,负责将ViewModel和ViewManger进行绑定,
进行数据转发工作。把合适的数据模型分发给合适的视图管理者。
这样的架构设计,就像一条生产线,ViewModel进行数据的采集和加工,Controller则进行数据的装配和转发工作,ViewManger进行接收转发分配来的数据,从而进行负责View的展示工作和管理View的事件。这样,不管哪个环节,都是可以更换的,同时也提高了复用性。
总结
iOS App是一个麻雀虽小,五脏俱全的软件。良好的架构和设计能够让代码容易理解和维护,并且不易出错。但是本文可能也存在错误之处,或者不足之处,希望大家看到有问题的地方在下方留言一起谈论学习,后续可能会持续更新更正本文。
参考文章: https://github.com/lovemo/MVVMFramework/tree/master/source MVVM与Controller瘦身实践 iOS 关于MVC和MVVM设计模式的那些事 iOS 关于MVVM Without ReactiveCocoa设计模式的那些事