一、需求背景
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、类图
image2、代码原理剖析
1)、A 与 B 双向数据绑定,Ain数据变化更新Aout、Bout数据,Bin同理。
image2)、有时候 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效果
主要数据绑定链有:
- 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(显示释放中奖,播放数字闪动动画)
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)
视频资料
如果您觉得还不错,麻烦在文末 “点个赞” 或者 下方评论 ,谢谢您的支持
查看原文
网友评论