尝试在工程中使用MVVM和ReactiveObjC。
引入MVVM是为了将界面和逻辑分开,将界面接口,比如label.text,转化为相应view model的数据属性接口。
引入ReactiveObjC主要是为了优雅地进行Controller和view model之间的双向绑定。
关于ReactiveObjC,这篇文章很有用:
iOS开发之ReactiveCocoa下的MVVM
目标页面
登录模块是基础模块之一。
- 以验证码方式登录

- 以账号密码方式登录

两种登录方式可以切换,在同一个界面。并且两种方式的共同点很少。
当前的阶段:需求分析和页面交互已经评审通过了,但是后台接口还没有出来。
实现方式
可以代码写界面,这个无话可说。只是界面代码真的很无聊,所以不选。
用一套界面实现,一个组件会充当两种功能,比如同一个输入框,一会儿是手机号,一会是账号,逻辑比较复杂。这样做,还不如直接代码写界面。
这个页面,上面部分是固定的,下面部分可变。可以考虑将下面部分分成两个位置重叠的
view。这个方案比上面两个都好多了,不过界面重合在一起,看不清楚,故事版所见即所得的优势发挥不出来。引入
Container View将重叠的部分平铺开来,感觉会好很多。 布局之后的样子大概是这样的:

基本上已经很像了,只是现在还不能切换。剩下的就需要代码来动态控制了。
引入
Container View之后,复用级别就从view改成了controller,形成1父2子三个controller。和子view类似,子controller可以由父controller持有,从而建立相互之间的关系。
#import "KJTPasswordLoginChildViewController.h"
#import "KJTCodeLoginChildViewController.h"
@interface KJTLoginViewController ()
// child controller
@property (strong, nonatomic) KJTPasswordLoginChildViewController *passwordController;
@property (strong, nonatomic) KJTCodeLoginChildViewController *codeController;
@end
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
if ([segue.identifier isEqualToString:@"toPassword"]) {
self.passwordController = segue.destinationViewController;
}
if ([segue.identifier isEqualToString:@"toCode"]) {
self.codeController = segue.destinationViewController;
}
}
引入MVVM
引入
MVVM会增加文件数量,这是不好的地方。将界面和逻辑分开,给
controller减负,这是好的地方。将界面元素接口,转化为
view model的数据属性接口,这是好的地方。现在后台接口文档还没出来,接口的名字和字段都不知道,引入一个
view model,就可以把整个交互逻辑串起来了,这是好的地方。等以后接口文档好了,接口名字和接口字段定了,只要做一下接口字段到
view model的属性名字映射就可以了,界面不用改动。这种解耦的方式,很适合现在的场景。
结论:综合考虑,对于这个登录页面,在当前的场景下,引入MVVM是有利的。
文件结构
在交互图上,是一个界面,只是通过类似的
tab切换,是界面重合,导致开发复杂化。为了很好地利用故事版所见即所得的优势,我们用3个controller来描述这一个界面。并没有规定说一个
controller要配一个独立的view model。虽然是3个不同的controller,但是实际上,可以把他们看做是一个。对于这3个强关联的controller,完全可以共用一个view model将上面所讲的两个观点通过文件夹结构来提现,大概是这个样子的:

引入ReactiveObjC
ReactiveObjC是大名鼎鼎的Reactive Cocoa的Object-C版本一提到
ReactiveObjC,就提到函数式编程,说是虽然效果好,但是学习曲线很陡峭,让很多人陷入纠结。其实,ReactiveObjC不需要函数编程,也是能够使用的。ReactiveObjC把所有的交互方式都统一成了信号,感觉很高深的样子。其实想点击相应,消息什么的,原生的API已经很好用,并需要转换。如果不用
ReactiveObjC,那么从view model来更新界面这一块就麻烦。首先要提供一个类似updateInterface之类的函数,然后,在需要改变界面的时候调用,很繁琐如果用
ReactiveObjC,那么可以建立controller和view model之间的属性绑定,不需要到处调用updateInterface,感觉好多了。在这里,只需要用到
RAC和RACObserve,真的很简单。
结论:引入ReactiveObjC,建立controller和view model之间的属性绑定,当view model改变的时候,可以优雅地在界面上提现出来。
View Model定义
view model是用来描述界面的,相当于界面的接口。这里,我们只用来描述父controller。这里的界面元素可以抽象为以下几种:
- 文字的颜色,选中是蓝色的,不选中是黑色的;
- 指示线的显示和隐藏;
- 代表子
controller的container view的显示和隐藏; - 当前用户选择了那种登录方式;
- 通过数据属性,来描述上面提到的几点界面特征,定义如下:
typedef NS_ENUM(NSInteger,KJTLoginType) {
KJTLoginTypeCode = 1,
KJTLoginTypePassword = 2,
};
@interface KJTLoginViewModel : NSObject
/*
* KJTLoginViewController
*/
// 登录方式
@property (assign, nonatomic) KJTLoginType loginType;
// 验证码标签颜色
@property (strong, nonatomic) UIColor *codeLabelColor;
// 隐藏验证码标签横线
@property (assign, nonatomic) BOOL hideCodeLine;
// 隐藏验证码登录容器
@property (assign, nonatomic) BOOL hideCodeContainer;
// 账号密码标签颜色
@property (strong, nonatomic) UIColor *passwordLabelColor;
// 隐藏账号密码标签横线
@property (assign, nonatomic) BOOL hidePasswordLine;
// 隐藏账号密码登录容器
@property (assign, nonatomic) BOOL hidePasswordContainer;
/*
* KJTPasswordLoginChildViewController
*/
/*
* KJTCodeLoginChildViewController
*/
@end
登录方式和其他的属性是有关系的,所以在创建
view model的时候就可以将这种关系固定下来。这里使用ReactiveObjC会显得非常优雅。建立了登录方式和其他属性之间的关系之后,默认值就非常方便了,只要设置登录方式一项就可以了,其他的,自然有
ReactiveObjC对应设置,非常方便。引入
ReactiveObjC之后,view model的代码可以写得非常优雅:
#import "KJTLoginViewModel.h"
@implementation KJTLoginViewModel
#pragma mark - life cycle
- (instancetype)init {
self = [super init];
if (self) {
[self bindLoginType];
[self setDefaultValue];
}
return self;
}
#pragma mark - private
- (void)bindLoginType {
[RACObserve(self, loginType) subscribeNext:^(id _Nullable x) {
KJTLoginType loginType = (KJTLoginType)[x integerValue];
switch (loginType) {
case KJTLoginTypeCode:
self.codeLabelColor = kBlueColor;
self.hideCodeLine = NO;
self.hideCodeContainer = NO;
self.passwordLabelColor = kBlackColor;
self.hidePasswordLine = YES;
self.hidePasswordContainer = YES;
break;
case KJTLoginTypePassword:
self.codeLabelColor = kBlackColor;
self.hideCodeLine = YES;
self.hideCodeContainer = YES;
self.passwordLabelColor = kBlueColor;
self.hidePasswordLine = NO;
self.hidePasswordContainer = NO;
break;
default:
break;
}
}];
}
- (void)setDefaultValue {
self.loginType = KJTLoginTypePassword;
}
@end
控制器属性
输出口:故事版只能表达静态页面,可变的动态页面,需要拉输出口到
controller中,进行动态控制。view model:作为controller的一个属性成员,为controller处理交互逻辑,页面逻辑方面的工作。属性的定义大概是这个样子的:
@interface KJTLoginViewController ()
// 验证码登录
@property (weak, nonatomic) IBOutlet UILabel *codeLabel;
@property (weak, nonatomic) IBOutlet UIView *codeLine;
@property (weak, nonatomic) IBOutlet UIView *codeContainer;
// 账号密码登录
@property (weak, nonatomic) IBOutlet UILabel *passwordLabel;
@property (weak, nonatomic) IBOutlet UIView *passwordLine;
@property (weak, nonatomic) IBOutlet UIView *passwordContainer;
// view model
@property (strong, nonatomic) KJTLoginViewModel *viewModel;
@end
绑定View Model
view model的属性变化要反映在界面上,那么就需要建立代表界面的输出口和view model属性之间的绑定工作。有ReactiveObjC的帮助,这将会非常简单:
#pragma mark - private
- (void)bindViewModel {
self.viewModel = [[KJTLoginViewModel alloc] init];
RAC(self.codeLabel, textColor) = RACObserve(self.viewModel, codeLabelColor);
RAC(self.codeLine, hidden) = RACObserve(self.viewModel, hideCodeLine);
RAC(self.codeContainer, hidden) = RACObserve(self.viewModel, hideCodeContainer);
RAC(self.passwordLabel, textColor) = RACObserve(self.viewModel, passwordLabelColor);
RAC(self.passwordLine, hidden) = RACObserve(self.viewModel, hidePasswordLine);
RAC(self.passwordContainer, hidden) = RACObserve(self.viewModel, hidePasswordContainer);
}
登录方式切换
由于界面的元素输出口已经和view model的属性进行了绑定; view model的其他属性已经和登录方式属性进行了绑定。所以,当用户切换登录方式时,只要修改view model的登录方式属性loginType就可以,简单易懂:
- (IBAction)codeButtonTouched:(id)sender {
self.viewModel.loginType = KJTLoginTypeCode;
}
- (IBAction)passwordButtonTouched:(id)sender {
self.viewModel.loginType = KJTLoginTypePassword;
}
运行效果
- 由于
view model中的默认值是以“账号密码方式登录”,所以默认界面是这样的:
- (void)setDefaultValue {
self.loginType = KJTLoginTypePassword;
}

- 切换到“验证码方式登录”,界面是这样的:

小结: 引入ReactiveObjC之后,以前相对比较繁琐的“tab切换”页面,可以非常简洁的实现。