一、需要背景
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_架构模式
如果您感觉还不错,麻烦在文末“点个赞”或者 下方评论,谢谢您的反对
查看原文