ZZActionPipe应用:实现一个响应式MVVM框架

什么是ZZActionPipe(iOS响应式编程框架)

利用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结构图

如上图,在没有引入“响应式”这个概念前,MVVM中的ViewController和ViewModel依旧需要需要相互依赖,VC和VM之间需要定义若干方法和代理,使其能够进行数据交换。

2、什么是响应式?

响应式网上有很多解释的文章,这里就总结下我的理解:
响应式是两个或多个模块间,在不相互依赖的前提下,通过某种媒介或中间件,实现有序通讯的编程方式。

不借助其他框架的情况下,一般我们可以使用KVO、NSNotification、runLoopSource等来实现响应式。KVO、NSNotification、runLoopSource就是两个模块之间的媒介,两个模块只需要跟媒介交互,而不是模块间直接交互。通过响应式媒介,我们就可以解除上述ViewController和ViewModel之间的依赖,得到如图结构:


响应式的MVVM结构图

当然KVO、NSNotification、runLoopSource还不能完全解除VC和VM的依赖,我们还需要借助功能更强大的媒介来实现响应式的MVVM。

3、ZZActionPipe实现响应式MVVM

有了以上认知后,我们尝试借助ZZActionPipe作为响应式媒介,构建MVVM框架。让ViewController和ViewModel各自持有一个pipe,定义好pipe的事件后,将两个pipe相连。两个模块就可以只跟各自的pipe交互,从而彻底解除VC和VM之间的依赖。


ZZActionPipe 响应式MVVM

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的工作。并独立出了一个埋点模块,实现无侵入式的曝光埋点和点击埋点。

MVVM CollectionView Demo

git项目

项目地址GitHub: https://github.com/q1992077/ZZActionPipe

PS:如非特别说明,所有文章均为原创作品,著作权归作者(袜子不分左右zz
)所有,转载请联系作者获得授权,并注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容