主要讲述的是MVVM在一些具体场景的业务实践
1. 首先讲一点是当我们讲MVVM的时候很多人觉得 MVVM的主要作用是如何避免把View Controller 写成 Massive View Controller ,但其实MVVM并不只是简单的解决Controller(后面简称C)的臃肿的问题(实际上如果正确和严格地使用 MVC 架构也不会造成vc臃肿),MVVM 在业务中除了解决Controller臃肿过重的问题之外还有很多重要的优势
<1>MVVM的介绍
当我们讨论app架构的时候,其实所想要解决的问题本质在于,我们要如何才能更清晰地管理“用户操作,模型变更,UI 反馈”这一“数据流动”的方式(这也是一直在强调的,如果不从根本上理解“数据流动”在 mvvm 中的角色,就有可能从vc的臃肿换成viewmodel的臃肿,或者更严重点多了一层胶水代码增多了, 并且依然没有解决vc的臃肿,反而在实际使用的更麻烦了)。mvvm或者说数据流动的最重要也是最核心的一点就是使用数据绑定的技术,使得当viewmodel 变化时候,view也会随之变化,从而实现了将视图和逻辑分离
在我之前的项目经常会遇到 同一个大模块下不同的业务之间ui的展示是相近的或者完全一样,这样借助mvvm 的优势可以实现ui组件的复用 。
<2>MVVM的写法实践
mvvm的数据绑定是比较重要的的一个环节,所以我们在业务的基础上 总结大约5种数据绑定方式
1. 单向数据绑定
2.双向数据绑定
3.集合的数据绑定
4 执行过程绑定
5 错误处理
基本着五种绑定能够覆盖业务中的大部分的场景
我们举简单demo例子去讲解这五种操作(以下都是结合RAC ,mvvm也可以不用RAC)
1.单向绑定
我们的app 基本上都会有纯粹展示数据的业务,比如支付宝产看账单 ,查看余额,或者网易新闻中我们查看新闻详情 新闻列表等等,这是一种很常见也是相对比较简单的业务
单向绑定其实是view到viewmodel 或者viewmodel到view的过程,比如我们把viewmodel的一个属性值能够显示在相应的view上,单向绑定一般应用的场景是 view的属性直接和viewmodel的属性关联起来,这样子我们可以通过修改viewmodel的方式改变view的展示的数据值,
看下面代码
// UI定义
@interface BalanceCell : MVVMBaseCell
@property (strong, nonatomic) UILabel *balanceLabel;
@end
// ViewModel定义
@interface BalanceViewModel : BaseViewModel
@property (strong, nonatomic) NSString *balance;
@end
// 数据绑定 (我们一般把数据绑定放在view中)
- (void)bindViewModel:(BalanceViewModel *)viewModel {
RAC(self.balanceLabel, text) = RACObserve(viewModel, balance)
}
这样的的绑定就是我们修改viewmodel的mobile的时候 view的mobileLabel也会随之变化
2.双向数据绑定
双向比较复杂点,一方面将viewmodel的属性显示view上,一方面也会对view的数据改变并同步到viewmodel,
我们的app 基本上都会有注册的业务,基本上都是包含用户输入姓名和手机号的功能
// UI定义
@interface MobileCell : MVVMBaseCell
@property (strong, nonatomic) UITextField *mobileTextField;
@end
// ViewModel定义
@interface MobileViewModel : BaseViewModel
@property (strong, nonatomic) NSString *mobile;
@end
// 数据双向绑定 我们一般把数据绑定放在view中)
- (void)bindViewModel:(MobileViewModel *)viewModel {
RACChannelTerminal *channelTerminal = self.mobileTextField.rac_newTextChannel;
RACChannelTerminal *channelTerminal2 = RACChannelTo(viewModel, mobile);
[channelTerminal subscribe:channelTerminal2];
[channelTerminal2 subscribe:channelTerminal];
}
3.组合数据绑定
组合数据一般常见于多个组件之间的联动,换句话说就是他操作不是单个数据源 而是多个数据源合并的结果,举例房贷金融计算器的业务 贷款的结果会根据你输入的贷款期限,贷款金额,还款方式(等额本金还是等额本息)而变化, 看下面代码
// 信号组合
- (void)combineAmountViewModel:(AmountViewModel *)amountVM
timeViewModel:(TimeViewModel *) timeVM
methodViewModel:(MethodViewModel *)methodVM
{
NSArray *signals = @[amountVM.amount,timeVM.time,methodVM.method];
_textSignal = [RACSignal combineLatest:signals
reduce:^id (NSString *amount, NSString *time, NSString *method)
{
return [NSString stringWithFormat:@"计算结果 = 总贷款%@ 还款期限:%@ 还款方式%@ ",amount,time,method]}];
}
}
4.执行过程绑定
执行过程绑定一般跟按钮或者cell的点击操作有关,我们会把按钮或者cell的点击操作行为和对应的代码关联绑定起来,这其实就是执行过程绑定。举个例子 我们经常在一个tableview 列表中 左滑删除一个cell 导致view的列表的的数量有所改变,或者我们还以注册的,一般会有点击清空的按钮 ,看下面代码
// UI定义
@interface OrderDetailCell : MVVMBaseCell
@property (strong, nonatomic) UIButton *clearButton;
@end
// ViewModel定义
@interface OrderDetailViewModel : BaseViewModel
@property (strong, nonatomic) RACCommand *clearCommand;
@end
// 执⾏信号创建
_clearCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
nameVM.name = nil;
phoneVM. phone= nil;
return [RACSignal empty];
}];
// 操作绑定
self.clearButton.rac_command = viewModel.clearCommand;
5.错误处理
错误处理会把一些不同的来源或者不同的错误处理,我们结合上面的例子的执行绑定扩展一下,在信号创建中我们加了一些判断,我们都会再点击提交的时候做一个判断(比如用户名或者手机号为空的时候)提示一个错误信息。
// 错误信号创建
_submitCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
if (nameInputVM.inputText == nil) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"姓名不能为空!"};
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:-1 userInfo:userInfo];
return [RACSignal error:error];
} else if (phoneInputVM.inputText == nil) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"手机号不能为空!"};
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:-1 userInfo:userInfo];
return [RACSignal error:error];
}
return [RACSignal empty];
}];
// 信号绑定
self.submitButton.rac_command = viewModel.submitCommand;
@weakify(self)
[self.submitButton.rac_command.errors subscribeNext:^(NSError *x) {
@strongify(self)
self.textView.text = x.localizedDescription;
}];
<3>MVVM的代码复用
看上面的图,这里面是两个不用的业务一个是 TVL 一个是HTL, 从左往右看,但我们看到view层是公共的 ,我们看viewmodel 这个模块,可以看到viewmodle 根据业务分成两个部分,但是通用一个baseviewmodel ,baseviewmodel是这里面是关于ui的一些属性(跟业务逻辑无关)
还是刚才的例子 注册的时候其实有两个cell(都包含一个label 和一个textfiled),但是他们校验逻辑有不一样(一个是校验姓名逻辑还有一个校验手机号逻辑)那其实这个cell是可以复用的
<4>MVVM的自动化测试
这边讲的如何针对viewmodel层自动化测试,mvvm的另一个好处就是基于上面的数据绑定,我们只需要测试到viewmodle这一层就是覆盖到ui上的体现
我们举个例子 看下面代码
_verifyPhoneSignal = [RACObserve(self, inputText) map:^id (NSString *phone) {
NSString *phoneRegexp = @"^1(3[0-9]|5[0-35-9]|8[0-25-9])\\d{8}$";
NSPredicate *regextestmobile = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", phoneRegexp];
return @((BOOL)[regextestmobile evaluateWithObject:phone]);
}];
// 测试“校验⼿手机号”逻辑是否正确
- (void)testVerifyPhone {
PhoneInputViewModel *viewModel = [[PhoneInputViewModel alloc] init];
RACChannel *channel = [[RACChannel alloc] init];
[viewModel.inputChannel subscribe:channel.leadingTerminal];
[channel.leadingTerminal subscribe:viewModel.inputChannel];
[channel.followingTerminal sendNext:@"18612345678"]; // 模拟从⽂文本框输⼊入 18612345678
NSNumber *verifyPhoneResult = [viewModel.verifyPhoneSignal first];
XCTAssertEqualObjects(verifyPhoneResult, @(YES));
[viewModel setValue:@"13810001000" forKey:@"inputText"]; // 模拟ViewModel更更新phone值为13810001000
XCTAssertEqualObjects([channel.followingTerminal first], @"13810001000"); // 检验⽂文本框内容是否为13810001000
}
<5>MVVM without RAC
使用 MVVM 最舒服的姿势是搭配 ReactiveCocoa(目前了解的 美团,知乎,蘑菇街都是采用MVVM + RAC),但是RAC 的学习成本和侵入性都比较高,这点就会导致已经开发人数比较多有自己固定的开发模式的团队中很难推起来了,所以如何不借助 ReactiveCocoa 来实现 MVVM。
RAC 是基于 KVO 构建的,所以也可以用 KVO 来让 VC 获取 VM 的变化。
但我们都知道 KVO 的槽点比较多,比如使用起来不方便,用完还要记得移除等。这里可以使用 Facebook 开源的 KVOController,它比较好的处理了 KVO 存在的一些问题,同时又能发挥 KVO 带来的便捷性。
网友评论