乐趣区

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

一、需要背景

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_架构模式

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

退出移动版