美文网首页
iOS链式语法数据绑定轻量级框架实践

iOS链式语法数据绑定轻量级框架实践

作者: 编程怪才_凌雨画 | 来源:发表于2020-12-08 14:30 被阅读0次

    一、需求背景

    1、现状

    当前组件化开发中,经常会用到MVVM设计模式,它促进了UI代码与业务逻辑的分离,一定程度解决viewController臃肿问题,但也使得数据绑定变得复杂,很多情况下需要我们手动绑定数据和刷新界面,经常要写一堆零散的数据绑定业务代码。

    关于数据绑定的复杂度问题,我们完全可以使用ReactiveCocoa框架(一个典型的函数响应式编程框架)解决,这里不做深入了解,它虽然很好很强大,但对于组件化开发来说还是供过于求,目前我们仅仅需要一个轻量级的数据绑定框架。

    2、目标

    自己维护一个轻量级的数据绑定开源框架,例如CRDataBind(Chain Response Data Bind),它的接口调用支持链式语法,并通过响应式编程快速实现数据绑定更新。

    (本方案主要以老虎机抽奖demo对其进行实践和分析,所用数据绑定框架原出自:github.com/shidavid/DV…,非常感谢作者的贡献!)

    二、解决方案及亮点

    1、方案概述

    • 使用链式编程,支持多项绑定,支持单向/双向数据流;
    • 支持过滤,某些条件下不更新绑定的数据;
    • 支持数值与字符串自动转换,以及自定义数据接收格式;
    • 只要支持KVC的对象都能实现数据绑定,不限定只能View和ViewModel;
    • 无需依赖第三方,无需手动解绑,当目标对象内存释放时,CRDataBind自动解绑和释放。

    2、问题难点

    1)、如何通过链式语法一次绑定多个对象?

    2)、如何通过响应式编程实现数据绑定?

    3)、如何实现自动解绑?

    3、分析过程

    1)、链式语法

    在Objective-C中,我们调用方法一般使用“[]”,简单的调用看起来过得去。但如果叠加很多层调用后,便不易阅读,常有漏掉某个“]”或“[”报错情况。

    链式语法的核心是点语法。为了让OC在进行多层方法调用时,能够优雅和清晰的展示代码,我们可以借鉴Swift、Masonary等的点语法形式。

    示例:

        // Swift:获取文件路径
        let path: String = Bundle.main.path(forResource: "image",     ofType: "jpg")!  
    
        // Objective-C:Masonary布局更新
        [self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
            make.center.equalTo(self);
            make.width.equalTo(@(self.buttonSize.width)).priorityLow();
            make.height.equalTo(@(self.buttonSize.height)).priorityLow();
            make.width.lessThanOrEqualTo(self);
            make.height.lessThanOrEqualTo(self);
        }];
    

    点语法的关键是block,可借鉴Swift闭包的使用。它的特殊在于其本身可以帮助方法进行参数传递,并返回数据,这样我们便可以让方法不断返回实例本身,继续调用实例方法。

    示例:

        /**绑定3
         model.winCode <---> winCodeLb.text <---> winCodeTF.text
         增加fiter过滤中奖号大于3位数的响应
         */
        CRDataBind
        ._inout(self.lotteryVM.currentLottery, @"winCode")
        ._out(self.winCodeLb, @"text")
        ._inout_ui(self.winCodeTF, @"text", UIControlEventEditingChanged)
        ._filter(^BOOL(NSString *text) {
            // 增加过滤:中奖号码不超过3位数,界面上无法配置大于3位数
            return text.length <= 3;
        });
    

    2)、响应式编程实现数据绑定

    响应式编程是一种面向数据流和变化传播的编程范式,数据的输入输出(in&out)是关键,绑定-响应-刷新。数据inout的形式有:普通对象如target.property = value;UI对象如textField.text响应EditingChanged等等。

    设想在同一个chain(响应链)中,我们需要一个观察者,观察者通过弱引用缓存所观察对象。然后,监听普通对象,可以使用KVO;监听UI对象时,绑定对应UI事件。那么chain上所观察的某个对象属性变化时,我们就可以遍历所有观察对象通过KVC(setValue:forkey:)进行更新操作。

    3)、实现自动解绑

    经过上面的分析,我们基本能实现接口的调用和实际数据绑定。接下来思考下:既然有绑定过程,那么对应的解绑也应该提供,而且最好是自动解绑,不需要外部手动去调用解绑和释放缓存。

    应该如何触发解绑过程?比如target是进行数据绑定的对象,那么正常逻辑是target释放了,或者主动调用才进行解绑操作。我们需要捕获对象释放,现成的方式是利用dealloc方法,但我们的目的是自动解绑,所以不应在绑定的所有外部对象dealloc中调用解绑。于是我们可以考虑为所有target实现一个NSObject分类,并通过runtime关联一个targetModel,当target释放后,model也跟着释放,此时我们便可以在targetModel的dealloc中调用unbindWithTarget:进行解绑和释放缓存的操作。

    三、详细设计

    1、类图

    image

    2、代码原理剖析

    1)、A 与 B 双向数据绑定,Ain数据变化更新Aout、Bout数据,Bin同理。

    image

    2)、有时候 A 与 B 双向绑定,B 与 C 双向绑定,其实相当于 A、B、C 一起绑定在一条数据链Chain上,每当有一个in数据变化,发送新数据到C

    hain上,再由Chain更新所有的out数据。

    这样实现单向/双向数据流。

    3)、利用KVO和UI(addTarget:)事件,数据链就相当于Obverse,每个Observer用一个ChainCode标记,Observer观察每个in数据变化,并更新到所有out数据。

    4)、主要对外接口阐述

    链式语法调用的API,必须以 _inout 或 _in 开头(肯定要有数据in来源,不然后续也没意义),后面的绑定顺序可随意,不影响绑定结果。

    • _inout 发送+接收数据
    • _in 只发送数据
    • _out 只接收数据
    • _cv 进行自定义数据转换后再返回
    • _filter 条件过滤
    • _out_key_any 绑定自定义事件
    • _out_not 接收的数据取反再返回

    具体接口如下:

    #pragma mark - 双向绑定
    + (DataBindBlock)_inout;
    + (DataBindUIBlock)_inout_ui;
    + (DataBindConvertBlock)_inout_cv;
    + (DataBindUIConvertBlock)_inout_ui_cv;
    
    - (DataBindBlock)_inout;
    - (DataBindUIBlock)_inout_ui;
    - (DataBindConvertBlock)_inout_cv;
    - (DataBindUIConvertBlock)_inout_ui_cv;
    
    #pragma mark - 单向绑定-发送(数据更新,只发送新数据,不接收)
    + (DataBindBlock)_in;
    + (DataBindUIBlock)_in_ui;
    
    - (DataBindBlock)_in;
    - (DataBindUIBlock)_in_ui;
    
    #pragma mark - 单向绑定-接收(数据更新,只接收新数据,不发送)
    - (DataBindBlock)_out;
    - (DataBindConvertBlock)_out_cv;
    - (DataBindBlock)_out_not;
    - (DataBindKeyAnyOutBlock)_out_key_any;
    
    #pragma mark - 过滤
    - (DataBindFilterBlock)_fil
    

    四、使用示例

    设置数据绑定一般放在胶水层(ViewController)中进行,具体可结合自身设计模式灵活运用。

    导入头文件:

    #import "CRDataBind.h"
    

    进行单向/双向数据绑定(label-只接收数据,model-即发送也接收数据响应):

        /**绑定
         model.winRate <---> rateLb.text <---> rateSlider.value
         */
        CRDataBind
        ._inout(self.lotteryVM.currentLottery, @"winRate")
        ._inout_ui(self.rateSlider, @"value", UIControlEventValueChanged)
        ._out(self.rateLb, @"text");
    

    根据条件过滤,未达到条件不处理响应:

        /**绑定
         model.winCode <---> winCodeLb.text <---> winCodeTF.text
         增加fiter过滤中奖号大于3位数的响应
         */
        CRDataBind
        ._inout(self.lotteryVM.currentLottery, @"winCode")
        ._out(self.winCodeLb, @"text")
        ._inout_ui(self.winCodeTF, @"text", UIControlEventEditingChanged)
        ._filter(^BOOL(NSString *text) {
            // 增加过滤:中奖号码不超过3位数,界面上无法配置大于3位数
            return text.length <= 3;
        });
    

    自定义数据接收格式:

        /**绑定
         model.sn <---> snLb.text <---> self.view.backgroundColor
         其中backgroundColor需要转换输出格式
         */
        CRDataBind
        ._inout(self.lotteryVM.currentLottery, @"sn")
        ._out(self.snLb, @"text")
        ._out_cv(self.view, @"backgroundColor", ^UIColor *(NSNumber *num) {
            NSInteger index = num.integerValue % kBGColors.count;
            return kBGColors[index];
        });
    

    绑定自定义事件:

       /**绑定
         model.isWin <---> isWinLb.text <---> self.isWin
         增加外部自定义事件,中奖后让抽奖号码闪烁
         */
        __weak __typeof(&*self) weakSelf = self;
        CRDataBind
        ._inout(self.lotteryVM.currentLottery, @"isWin")
        ._out(self.isWinLb, @"text")
        ._out_key_any(@"202122", ^(NSNumber *num) {
            weakSelf.isWin = num.boolValue;
            NSLog(@">>>在setIsWin:中触发中奖时号码闪烁,iswin = %d", weakSelf.isWin);
        });
    

    五、成效举证

    针对本案制作了CRDataBindDemo,它是一个老虎机摇号抽奖程序,通过MVVM + CRDataBind链式响应编程,快速地完成了多个带界面交互的数据绑定业务。

    1、demo效果

    地址:github.com/stkusegithu…

    主要数据绑定链有:

    • model.sn <---> snLb.text <---> self.view.backgroundColor(期号递增显示不同背景色)
    • model.winRate <---> rateLb.text <---> rateSlider.value(滑动slider改变中奖率)
    • model.code <---> codeLb.text(抽奖后显示抽奖号码变化)
    • model.winCode <---> winCodeLb.text <---> winCodeTF.text(设置下一期中奖号)
    • model.isWin <---> isWinLb.text <---> self.isWin(显示释放中奖,播放数字闪动动画)
    image image

    2、成效说明

    比如demo中,需要配置老虎机下一期中奖号码时,在未使用CRDataBind前的业务代码书写如下:

    - (void)setupBind {
        // 绑定textField编辑事件
        [self.winCodeTF addTarget:self    action:@selector(winCodeTFdidEdittingChanged:) forControlEvents:UIControlEventEditingChanged];
    
        // 未知的地方调用
        self.lotteryVM.currentLottery.winCode = @"222";
        [self freshWinCodeUI];
    }
    
    - (void)winCodeTFdidEdittingChanged:(UITextField *)textField {
        if (textField.text.length > 3) {
            textField.text = [textField.text substringToIndex:3];
            return;
        }
        self.lotteryVM.currentLottery.winCode = self.winCodeLb.text = textField.text;
    }
    
    - (void)freshWinCodeUI {
        // 刷新界面
        NSString *winCode = self.lotteryVM.currentLottery.winCode;
        self.winCodeLb.text = self.winCodeTF.text = winCode;
    

    可以看出上面比较零散和繁琐。再看看当我们使用CRDataBind后,是不是变得干净清爽多了:

    - (void)setupBind {
        /**绑定
         model.winCode <---> winCodeLb.text <---> winCodeTF.text
         增加fiter过滤中奖号大于3位数的响应
         */
        CRDataBind
        ._inout(self.lotteryVM.currentLottery, @"winCode")
        ._out(self.winCodeLb, @"text")
        ._inout_ui(self.winCodeTF, @"text", UIControlEventEditingChanged)
        ._filter(^BOOL(NSString *text) {
            // 过滤:中奖号码需小于3位数
            return text.length <= 3;
        });
    }
    

    六、核心代码范围

    代码位于目录 CRDataBindDemo/CRDataBindDemo/CRDataBind/下

    ---CRDataBind

    +---CRDataBindDefine.h

    +---CRDataBind.h

    +---CRDataBind.m

    +---CRDataBindObserverManager.h

    +---CRDataBindObserverManager.m

    +---CRDataBindObserver.h

    +---CRDataBindObserver.m

    +---NSObject+DataBind.h

    +---NSObject+DataBind.m

    +---CRDataBindObserverModel.h

    +---CRDataBindObserverModel.m

    +---CRDataBindTargetModel.h

    +---CRDataBindTargetModel.m

    推荐文章

    iOS 架构
    iOS获取调用链
    iOS架构模式(代理,block,通知,MVC,MVP,MVVM)

    视频资料

    iOS_架构模式

    如果您觉得还不错,麻烦在文末 “点个赞” 或者 下方评论 ,谢谢您的支持
    查看原文

    相关文章

      网友评论

          本文标题:iOS链式语法数据绑定轻量级框架实践

          本文链接:https://www.haomeiwen.com/subject/eynfgktx.html