尝试在工程中使用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切换”
页面,可以非常简洁的实现。