iOS: 基于KVO的轻量级iOS双向数据绑定响应式编程框架

前言:几年前空闲时间玩了Vue.js, 发现利用数据双向绑定,开发如此轻松简洁,我了解iOS也有类似的框架 ReactiveCocoa, ReactiveCocoa有点复杂和笨重,我只需要简单点的数据绑定,所以写了一个轻量级的数据绑定,麻烦大家看一下,有问题请指点下


1.Demo例子:

  • 一般配合MVVM架构使用,主要用于View和ViewModel双向绑定,也可用于其他数据双向绑定
  • 这里介绍下双向数据绑定好处:
    1. View.textField 跟 ViewModel.text 绑定,用户在输入框textField输入"Hello World",text也会响应式更新,此时text = @"Hello World", 我们只要对text进行处理
    2. 如果我们从网络获取Model,Model 转换并赋值于 ViewModel.text,View也会响应式更新界面, 整个过程都是对ViewModel.text进行操作,不会再去处理View部分

github地址: https://github.com/shidavid/DVDataBind
其他例子:利用 DVDataBind 双向绑定 + MVVM 简单实现登录界面

// 这里只是展示响应式变化 
DVDataBind
._inout(self.demoModel, @"text")
._inout_ui(self.demoView.textField, @"text", UIControlEventEditingChanged)
._out(self.demoView.label, @"text")

// 点击button
- (void)onClickForButton:(UIButton *)sender {
    self.demoModel.text = @"Hello World";
}
image

2.介绍:

  1. 不限定只能 View 与 ViewModel 绑定,只要支持KVC的数据都能双向绑定
  2. 使用链式编程,支持多项绑定
  3. 支持单向数据流/双向数据流
  4. 支持 字符串,整形,浮点型,布尔类型 之间数据自动转换 (对象类型除外)
  5. 支持过滤, 限制,转换, 观察数组某一位数据变化
  6. 无需继承基类,无需手动解绑, 当目标对象内存释放,DataBind自动解绑和释放内存

3.思路

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


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


    image

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


image
  1. 利用KVO, 数据链就相当于Obverse,每个Observer用一个ChainCode标记,Observer观察每个in数据变化,并更新到所有Out数据


    image

4.用法

  • DVDataBind 必须用 _in 或 _inout 开头, 后面绑定顺序先后随意, 任意组合, 不影响结果
  • _in 只发送新数据,_inout 可接受和发送新数据,_out 只接受新数据
  • 目标对象必须支持KVC
  • 目标对象不能为nil, property可为nil
  • Swift也能使用, 只不过更新数据不能直接 object.property = xxx , 需要 object.setValue(xxx, forKey: "property")
1. 普通绑定
/*
  object为目标对象, property是object拥有的属性
  object不能为nil,property可为nil
*/
._inout(object, @"property")


举例:
/*
  objectA -> a1, a2
  objectB -> b
  objectC -> c
  a1、a2、b、c 正常情况为同一类型, 如果不同类型查看下面 "3.转换"
*/
DVDataBind
._in(objectA, @"a1")
._inout(objectA, @"a2")
._inout(objectB, @"b")
._out(objectC, @"c");
2. UI 绑定
/*
  UI: 支持 UIControlEvents
  property: 通过触发 UIControlEvents 会产生数据变化的 属性
*/
._inout_ui(UI, @"property", UIControlEvents)


举例:
/* 
  view      -> UITextField *textField;
  viewModel -> NSString *text;
*/
DVDataBind
._inout_ui(view.textField, @"text", UIControlEventEditingChanged)
._inout(viewModel, "text");


/*
 view      -> UILabel *label;
 viewModel -> NSString *text;
 UILabel 不支持 UIControlEvents
*/
DVDataBind
._in(viewModel, "text");
._out(view.label, @"text");


/*
 view      -> UISwitch * switch;
 viewModel -> BOOL isON;
*/
DVDataBind
._inout_ui(view.switch, @"on", UIControlEventValueChanged)
._inout(viewModel, "isON");


/*
 view      -> UIImageView *imageView;
 viewModel -> UIImage *image;
 UIImageView 不支持 UIControlEvents
*/
DVDataBind
._in(viewModel, "image");
._out(view.imageView, @"image");


/*
 view      -> UISlider *slider;
 viewModel -> float value;
*/
DVDataBind
._inout_ui(view.slider, @"value", UIControlEventValueChanged)
._inout(viewModel, "value");


/*
 view      -> UIProgressView *progressView;
 viewModel -> float value;
*/
DVDataBind
._inout_ui(view.progressView, @"progress", UIControlEventValueChanged)
._inout(viewModel, "value");


/*
  view      -> UISegmentedControl *segmented;
  viewModel -> int index;
*/
DVDataBind
._inout_ui(view.segmented, @"selectedSegmentIndex", UIControlEventValueChanged)
._inout(viewModel, "index");


/*
 view      -> UIStepper *stepper;
 viewModel -> int index;
*/
DVDataBind
._inout_ui(view.stepper, @"value", UIControlEventValueChanged)
._inout(viewModel, "index");


/*
 view  -> UIButton *button;
 点击Button改变的是highlighted值,highlighted容易打错, 还是建议用 addTarget
*/
DVDataBind
._in_ui(view.button, @"highlighted", UIControlEventTouchUpInside)
._out_key_any(@"自定义", ^{
    // 点击触发
});
3.转换
  • 支持 字符串,整形,浮点型,布尔类型 之间数据自动转换 (对象类型除外)
/*
普通对象转换
ClassA objectA -> a;
ClassB objectB -> b;
*/
DVDataBind
._inout_cv(objectA, @"a", ^ClassA *(ClassB *变量) {
    // 处理程序
    return 转换为ClassA的数据更新 objectA.a;
})
._inout_cv(objectB, @"b", ^ClassB *(ClassA *变量) {
    // 处理程序
    return 转换为ClassB的数据更新 objectB.b;
} );


特殊情况:
/*
view      -> UITextField *textField;
viewModel -> NSString *text;
viewModel -> int number;
支持 字符串,整形,浮点型,布尔类型 之间数据自动转换 (对象类型除外)
如果text为非数字, 则number为0
*/
DVDataBind
._inout_ui(view.textField, @"text", UIControlEventEditingChanged)
._inout(viewModel, "text")
._inout(viewModel, "number"); 


/*
view  -> UITextField *textField;
view  -> UILabel *label;
viewModel -> NSString *text;
viewModel -> int number;
这里 更新值有 NSString, int 类型,上面说过这些类型之间自动转换
viewModel->text 获取更新值自动转为NSString, 处理完返回NSString 再去更新自己
viewModel->number 获取更新值自动转为NSNumber, 处理完返回NSNumber 再去更新自己
*/
DVDataBind
._inout_ui(view.textField, @"text", UIControlEventEditingChanged)
._out_cv(view.label, @"text", ^NSString *(NSString *text) {
    NSString *tempText = [NSString stringWithFormat:@"AAA - %@ - BBB",text];
    return tempText;
})
._inout_cv(viewModel, @"text", ^NSString *(NSString *text) {
    NSString *tempText = [NSString stringWithFormat:@"CCC - %@ - DDD",text];
    return tempText;
} )
._inout_cv(viewModel, @"number", ^NSNumber *(NSNumber *num) {
    int value = [num intValue];
    return @(value + 123456);
});

4.数组
/*
  objectA -> array
  array必须为NSMutableArray类型, 绑定前必须初始化, 数组可提前赋值, 也可以为空
*/
._inout_arr(objectA, @"array", 1)

// 更新数组某位必须该方法
NSMutableArray *pArray = [objectA mutableArrayValueForKey:@"array"];
pArray[0] = @(123456);
pArray[1] = @"Hellow World";  //这里更改了第1位数据, 响应
pArray[2] = object;
5.取反
// property类型为BOOL类型
._out_not(objectA, @"property");


举例:
/*
  view -> UITextField *textField;
  view -> UISwitch * switch;
  view -> UISwitch * switchNot;
 当textField.text长度不为0,  则switch.on = YES, switchNot.on = NO
 当 switch.on = NO, switchNot.on = YES
*/
DVDataBind
._in_ui(view.textField, @"text", UIControlEventEditingChanged)
._inout_ui(view.switch, @"on", UIControlEventValueChanged)
._out_not(view.switchNot, @"on")
6.输出Block
  • 支持绑定多个输出Block, 更新数据不支持自动转换
// 自定义名不能一样, 类型为更新数据类型 (类型、变量可不写)
._out_key_any(@"自定义名1", ^(Class 变量){
     // 处理代码1
 })
._out_key_any(@"自定义名2", ^(Class 变量){
     // 处理代码2
 })
._out_key_any(@"自定义名3", ^{
     // 处理代码3
 });


举例:
// 整形、浮点型、布尔类型,必须是NSNumber 类型
._out_key_any(@"自定义名", ^(NSNumber *num){ 
     // 处理代码
 });

//更新值只是NSString类型
._out_key_any(@"自定义名", ^(NSString *text){  
     // 处理代码
 });

// 如果更新数据类型多样,就用id
._out_key_any(@"自定义名", ^(id value){
    //判断value为哪个Class, 进行处理
 });
7.过滤,限制
  • 一个数据链只能绑定一个过滤, 更新数据不支持自动转换
  • 在这里可以对数据进行判断,限制,校验等等操作
._filter(^BOOL(Class 变量) {  
    // 这里可以对数据进行判断,限制,校验等等
    return YES/NO; // 返回YES 则正常数据更新, NO不更新
})


举例:
//更新值只是NSString类型
._filter(^BOOL(NSString *text) {  
    return text.length <= 20; //限制字符串长度为20
})

//更新值为整形、浮点型、布尔类型,必须是NSNumber类型
._filter(^BOOL(NSNumber *num) {  
    return [num intValue] <= 100; //限制数字最大为100
})

//如果更新值为多类型,例如有NSString,NSNumber, 则写id
._filter(^BOOL(id value) {  
    //判断value为哪个Class, 进行处理  
    return YES/NO;
})

8.中途增加绑定
// 一开始绑定生成一条数据链
DVDataBind
._inout(objectA, @"a")
._inout(objectB, @"b")

// 将objectBB.bb 加入 objectA.a 的数据链中,
// objectA.a、objectB.b、objectBB.bb在同一数据链上
DVDataBind
._inout(objectA, @"a")
._inout(objectBB, @"bb")

// 相当于
DVDataBind
._inout(objectA, @"a")
._inout(objectB, @"b")
._inout(objectBB, @"bb")
9.解绑
  • 已经支持 当对象内存释放自动解绑, 无需手动解绑, 如果想手动解绑可用以下API
// 解绑objectA的所有 property
[DVDataBind unbindWithTarget:objectA];
// 解绑objectA的 a
[DVDataBind unbindWithTarget:objectA property:@"a"];
// 解绑objectA的 控件a
[DVDataBind unbindWithTarget:objectA property:@"a" controlEvent:UIControlEventValueChanged];
// 解绑objectA的 数组a 的某位
[DVDataBind unbindWithTarget:objectA property:@"a" index:1];
// 解绑objectA的 a 所在数据链的 输出Block "XXX"
[DVDataBind unbindWithTarget: objectA property:@"a" outBlockKey:@"XXX"];

5.如何导入项目

  1. 编译DVDataBindKitShell


    image
  2. 生成Framework拖入项目


    image
  3. 项目 Target -> Build Settings -> Linking ->Other Linker Flags 添加参数: -all_load -ObjC


    image
  4. 在PCH文件导入

#import <DVDataBindKit/DVDataBindKit.h>

6.结语:

github地址: https://github.com/shidavid/DVDataBind
谢谢大家观看,有兴趣麻烦点个星星关注下 😁😁😁

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容