利用ZZActionPipe的能力,我们可以很轻松的构建一套响应式的MVVM编程框架。
1、什么是MVVM?
MVVM是Model-View-ViewModel的简写,是对MVC模式的进一步改造的编程框架。将C层分为ViewModel和ViewController两个部分,从而达到将视图逻辑与数据逻辑分离,使得两部分逻辑既相互独立又可以相互组合的目的。实现:
1、功能进一步抽象,方便管理。
2、模块高内聚,低耦合。(每个部分相互独立,尽量不直接引用对方)
3、模块可复用性强,可扩展性强。(因为模块间相互独立,功能自我闭环,所以可以相互组合)
对于Objective-C 来说是将逻辑分为 Model-ViewModel-ViewController-View这四个部分。各部分的职责是:
1、Model: 数据存储。
2、ViewModel: 操作Model,业务数据处理,网络请求,封装视图需要展示的内容等。
3、ViewController: 控制页面生命周期,填充组合视图,处理视图交互等。
4、View: 视图绘制,视图组合,设置视图交互。
将各部分组合起来就得到以下结构:
如上图,在没有引入“响应式”这个概念前,MVVM中的ViewController和ViewModel依旧需要需要相互依赖,VC和VM之间需要定义若干方法和代理,使其能够进行数据交换。
2、什么是响应式?
响应式网上有很多解释的文章,这里就总结下我的理解:
响应式是两个或多个模块间,在不相互依赖的前提下,通过某种媒介或中间件,实现有序通讯的编程方式。
不借助其他框架的情况下,一般我们可以使用KVO、NSNotification、runLoopSource等来实现响应式。KVO、NSNotification、runLoopSource就是两个模块之间的媒介,两个模块只需要跟媒介交互,而不是模块间直接交互。通过响应式媒介,我们就可以解除上述ViewController和ViewModel之间的依赖,得到如图结构:
当然KVO、NSNotification、runLoopSource还不能完全解除VC和VM的依赖,我们还需要借助功能更强大的媒介来实现响应式的MVVM。
3、ZZActionPipe实现响应式MVVM
有了以上认知后,我们尝试借助ZZActionPipe作为响应式媒介,构建MVVM框架。让ViewController和ViewModel各自持有一个pipe,定义好pipe的事件后,将两个pipe相连。两个模块就可以只跟各自的pipe交互,从而彻底解除VC和VM之间的依赖。
4、使用ZZActionPipe实现一个简单登录页
需求如下:
==> 界面由用户名和密码两个输入框和一个登录按钮组成。
==> 用户名为空 或 密码不足6位,登录按钮置灰不可点击。
==> 用户名不为空 且 密码输入超过6位时,登录按钮自动置红可点击。
==> 点击登陆按钮,判断登录是否成功。
首先我们将需求拆分为 视图逻辑 和 数据逻辑 两个部分,对应VC和VM所需要处理的需求:
ViewController:
- View展示布局。
- 输入用户名和密码时,用pipe将变化的信息发出。
- 点击登录按钮,用pipe发出点击消息。
- 通过pipe接收登录按钮是否可点击的响应。
- 通过pipe接收登录是否成功的响应。
ViewModel:
- 存储用户输入的用户名和密码。
- 通过pipe接收用户输入的响应,并判断输入是否合法,然后将判断结果通过pipe回传。
- 通过pipe接收登录按钮点击事件,并判断登录是否成功,然后将判断结果通过pipe回传。
接下来我们定义VC和VM之间需要通过pipe来响应的事件协议:
@protocol pipeActionProtocol
//⚠️ 用户输入变化协议,协议表述:用户名、密码的变化,以及用户名和密码是否合法。
- (void)filterLoginWithName:(NSString *)strName passWord:(NSString *)strPassWord faild:(BOOL)bFaild;
//⚠️ 用户登录按钮点击协议,协议表述:触发登录动作。
- (void)loginAction;
@end
VC和VM的pipe分别注册上述协议的方法:
首先VC实现上述协议,处理登录按钮是否可点击,和登录是否成功的展示。
@implementation ViewController2
- (void)filterLoginWithName:(NSString *)strName passWord:(NSString *)strPassWord faild:(BOOL)bFaild{
if (!bFaild) {
//⚠️ 用户名 密码 输入合法,登录按钮设置为可点击。
[self.btnLogin setEnabled:YES];
self.btnLogin.backgroundColor = [UIColor redColor];
}else {
//⚠️ 用户名 密码 输入不合法,登录按钮设置为不可点击。
[self.btnLogin setEnabled:NO];
self.btnLogin.backgroundColor = [UIColor grayColor];
}
}
- (void)loginAction {
ActionProcess *process = [ActionProcess getCurrentActionProcess];
if (process.state == k_action_success) {
NSLog(@"登陆成功!");
}else if(process.state == k_action_error) {
NSLog(@"密码错误!");
}
}
@end
将VC实现的两个响应方法注册到pipe中,实现事件响应订阅。
_vcPipe.registAction(@selector(filterLoginWithName:passWord:faild:)).delegate = self;
_vcPipe.registAction(@selector(loginAction)).state(k_action_success | k_action_error).delegate = self;
再用VC的pipe注册textFieldDidChangeSelection:
方法,并作为UITextField
的delegate,在UITextField
发生变化时,触发filterLoginWithName:passWord:faild:
//⚠️注册代理 UITextFieldDelegate
__weak typeof(self) weakSelf = self;
_vcPipe.registAction(@selector(textFieldDidChangeSelection:)).action = pipe_createAction(UITextField *textField) {
// ⚠️ 向pipe发出发生变化的用户名和密码
[(id<pipeActionProtocol>)weakSelf.vcPipe filterLoginWithName:weakSelf.textName.text
passWord:weakSelf.textPassWord.text
faild:YES];
};
// ⚠️ 让pipe成为代理
self.textName.delegate = (id<UITextFieldDelegate>)self.vcPipe;
self.textPassWord.delegate = (id<UITextFieldDelegate>)self.vcPipe;
同时,让VC的pipe成为UIButton的target,点击登录按钮时,触发loginAction
[self.btnLogin addTarget:self.vcPipe action:@selector(loginAction) forControlEvents:UIControlEventTouchUpInside];
在VM中同样向pipe注册filterLoginWithName:passWord:faild:
和loginAction
方法,来处理数据层的逻辑
vmPipe.registAction(@selector(filterLoginWithName:passWord:faild:)).action = pipe_createAction(NSString *strName, NSString *strPassWord, BOOL bFaild) {
//⚠️ 存储用户输入的 用户名、 密码
self.name = strName;
self.passWord = strPassWord;
ActionProcess *process = [ActionProcess getCurrentActionProcess];
//⚠️ 判断用户名是否为空 & 密码是否大于6位
if ((strName && strName.length > 0) && (strPassWord && strPassWord.length > 6)) {
NSLog(@"%@",strPassWord);
[process changeArgumentOld:&bFaild toNew:jd_tuple(NO)];
}
};
vmPipe.registAction(@selector(loginAction)).state(k_action_start).action = pipe_createAction(){
ZZActionPipe<pipeActionProtocol> *rootPipe = [ZZActionPipe<pipeActionProtocol> getRootPipe];
// ⚠️ 模拟登录接口请求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if ([self.passWord isEqualToString:@"00000000"]) {
[rootPipe.doWithState(k_action_success) loginAction]; //⚠️ 发出登录成功事件
}else {
[rootPipe.doWithState(k_action_error) loginAction]; //⚠️ 发出登录失败事件
}
});
};
组合VC和VM的pipe,形成事件响应链:
ViewController2 *vc = [ViewController2 new];
ZZActionPipe *vmPipe = [ViewMoel2 pipe];
[vmPipe addPipe:vc.vcPipe]; //⚠️ pipe组合,将VC的pipe接在VM的pipe之后
经过上述逻辑拆分,并用pipe组合后,我们可以清晰的将逻辑在模块内自我闭环,使得VC和VM完全解耦,并且VC和VM都具备触发响应和接收响应的能力。最终形成下图所示的两条事件链:
至此,我们就完成了一个响应式MVVM框架实现的小项目,ZZActionPipe可以定义任意事件,将任意个模块相连接。可以很方便的帮助我们将逻辑拆分细化,使得项目更易维护、兼容和扩展。
类似的,ZZActionPipe中还实现了一个CollectionView列表项目的demo,更是将VC和View进行分离,让Cell也拥有自己的pipe,处理自己的逻辑。并可以跟VC任意组装,如此一来VC就可以更专注处理VC的工作。并独立出了一个埋点模块,实现无侵入式的曝光埋点和点击埋点。
git项目
项目地址GitHub: https://github.com/q1992077/ZZActionPipe
PS:如非特别说明,所有文章均为原创作品,著作权归作者(袜子不分左右zz
)所有,转载请联系作者获得授权,并注明出处。