GICDataBinding 0.1.0

GICDataBinding 0.1.0

gonghaiwei维护。



  • 作者:
  • gonghaiwei

GICDataBinding

CI Status Version License Platform

简介

GICDataBinding 是一款基于 NSProxy 开发的数据绑定库,支持 数据绑定事件绑定(觉得好的朋友们,不要吝啬您的 star),具有以下特色功能:

  1. 支持 JS 表达式

    @"'姓名:'+$.name.split('').reverse().join('') + ',性别:'+($.isMale?'男':'女')"
    1. 表达式仅支持单一表达式,如果表达式字符串中出现多个表达式,可能会出现意外。
    2. 您可以在单一表达式中调用任意 JS 中的方法,甚至调用您 预先注入 的方法。这样一来就为基于数据绑定开发的功能增加了无限可能。您可以直接将一些以前在 ViewModel 中定义的方法直接注入到 JSCore 中,使得可以直接在 JS 表达中调用这些方法
    3. 您可以直接在 JS 表达式中对数据源中的属性进行计算,然后将结果返回。

    灵活基于 JSCore 开发、注入各种 方法Class 将会使得开发某些功能变得异常的简单。甚至如果您的部分 UI 是基于 Texture 这样的支持自动布局的库开发的话,那么对于构建 UI 这样的任务变得异常的简单。

  2. 单向绑定

  3. 双向绑定

    双向绑定在本质上还是基于单向绑定的,但是 GICDataBinding 对某些 UI 组件进行了封装,使得可以直接一行代码就能完成双向绑定。类似于前端 vue 中的 model

  4. 支持对 NSMutableArray 进行观察。

    当数组内容变更后,您可以得到相应的回调。这样一来您可以开发出类前端 VUE 那样的自动根据数组内容变更,从而更新 UI 的功能。

  5. 支持 事件绑定。(重点)

    当前对于 UIControl 已经实现了相关的事件绑定。其他的事件开发者可以自行实现。实现的方法也是很简单的。

    其技术基础是,基于 NSProxy 实现对方法调用的拦截,从而可以实现类似方法交换的目的。也就是说这里可以不通过方法交换技术就能实现类似的功能需求。PS:有兴趣的话可以看一下这部分源码,我相信一定会让您有所收获。

  6. 当然也支持 Swift 开发。但是要求 Swift 中的数据类必须是 NSObject 子类。

安装

pod 'GICDataBinding'

使用方法

数据模型

所有数据绑定功能的前提是一个可以被观察的对象,就像KVO那样的。但是GICDataBinding不是基于KVO开发的,而是基于NSProxy开发的,因此在进行数据绑定之前,需要对你的数据源做一个转换,将数据源变成可观察对象,这一切的基础是基于NSProxy实现的。

  1. 首先,你需要有一个数据类,比如UserInfo这样的数据模型

  2. 数据类必须实现GICObserverProtocol协议,这个协议实际上是一个空协议,仅仅是用来标记该类是可观察的。由于GICDataBinding支持嵌套,因此所有数据模型中的对象要想可以被观察,那么都需要实现GICObserverProtocol协议。

    @interface TestData : NSObject<GICObserverProtocol>
    @property(nonatomic,copy)NSString *name;
    @property(nonatomic,assign)NSInteger age;
    @property(nonatomic,assign)BOOL isMale;
    @property(nonatomic,strong)UserAddress *address;
    @property (nonatomic,strong)NSDictionary *dict;
    @property (nonatomic,strong)NSMutableDictionary *dict2;
    @property (nonatomic,strong)NSArray *array;
    @property (nonatomic,strong)NSMutableArray *mutArray;
    @property(nonatomic,assign)NSInteger timeTick;
    +(instancetype)testData;
    @end
  3. 对数据模型调用gic_observer方法,你会获得一个可以被观察的数据模型。

     TestData *data = [[TestData testData] gic_observer];

数据绑定

JS表达式中的$表示数据源本身,因此如果你想要访问数据源的某个属性或方法,那么必须使用$来访问

  1. 普通的单向绑定。

    [btn gic_addBinding:createDataBinding(theme, @"$.backgroundColor", ^(UIButton *target, id newValue) {
       [target setTitleColor:newValue forState:UIControlStateNormal];
    })];

    这样,当数据源中的backgroundColor属性改变的时候就会触发回调

  2. 直接将表达式绑定到目标对象的属性上。

    [lbl gic_addBinding:createPropertyDataBinding(user, @"'计数:'+$.timeTick", @"text")];

    通过createPropertyDataBinding可以创建一个属性绑定,将表达式'计数:'+$.timeTick的结果自动绑定到UILabletext属性。

    注意:createPropertyDataBinding在内部实现的时候基于setValue:forKey实现的,因此确保这个属性是支持KVC的。

    如果这个属性不支持,那么使用第一种方法也能达到数据绑定目的。

  3. 双向绑定。

    UISwitch *sw = [[UISwitch alloc] initWithFrame:CGRectMake(10, CGRectGetMaxY(btn.frame)+10, 100, 44)];
    [sw gic_towwayBinding:user propertyName:@"isMale"];
    [self.view addSubview:sw];

    你可以通过gic_towwayBinding来创建一个双向绑定。propertyName表示的是数据源中的属性名称。也就是说,当数据源中的isMale改变的时候,UISwitchisOn也会跟着改变。而当UISwitchisOn被改变的时候,数据源中的isMale属性也会跟着改变。但这里你不用担心会陷入死循环,类库已经做了相应的处理,如果两次变更的value是一样的,那么不会重复触发。

  4. 观察NSMutableArray

    第一步当然是将array转换成可观察对象。通过调用gic_observer来实现。

     arrayObserve.changedBlock = ^(NSMutableArray *mutArray, GICMutableArrayChangedType changedType, NSArray *params) {
                    switch (changedType) {
                        case GICMutableArrayChangedAddObject:
                            NSLog(@"GICMutableArrayChangedAddObject:%@",params[0]);
                            break;
                        case GICMutableArrayChangedRemoveObject:
                            NSLog(@"GICMutableArrayChangedRemoveObject:%@",params[0]);
                            break;
                        case GICMutableArrayChangedRemoveAllObjects:
                            NSLog(@"GICMutableArrayChangedRemoveAllObjects");
                            break;
                        case GICMutableArrayChangedRemoveLastObject:
                            NSLog(@"GICMutableArrayChangedRemoveLastObject:%@",params[0]);
                            break;
                        case GICMutableArrayChangedSortArray:
                            NSLog(@"GICMutableArrayChangedSortArray:%@",mutArray);
                            break;
                        case GICMutableArrayChangedInsertObject:
                            NSLog(@"GICMutableArrayChangedInsertObject:%@,index:%@",params[0],params[1]);
                            break;
                        case GICMutableArrayChangedReplaceObject:
                            NSLog(@"GICMutableArrayChangedReplaceObject:%@,index:%@",params[0],params[1]);
                            break;
                        default:
                            break;
                    }
                };

    目前GICDataBinding支持的观察方法的枚举都已经在上面列出。

  5. 在表达式中调用注入的JS方法。

    [GICJSContextManager manager].jsContext[@"customFunc"] = ^(JSValue*value){
       //这里以将Color 转换成字符串为例
       UIColor *color = [value toObject];
       CGFloat r,g,b,a;
       [color getRed:&r green:&g blue:&b alpha:&a];
       return [NSString stringWithFormat:@"r:%f,g:%f,b:%f",r,g,b];
    };
    // 数据绑定表达式直接调用js方法
    [lbl gic_addBinding:createPropertyDataBinding(theme, @"'颜色转换:'+customFunc($.titleColor)", @"text")];

    注入JS方法有两种方式

    1. 直接如下上面的方法,采用block方式注入

    2. 调用 [[GICExpresionCalculator calculator].jsContext evaluateScript:jsString] 注入JS脚本

事件绑定

GICDataBinding中的事件绑定只能调用JS代码,但是得益于JSCore能够直接调用本地代码,也就意味着你也可以直接在事件绑定中调用本地代码。

在做事件绑定之前,首先你要有一个ViewModel。同样,这个ViewModel需要实现GICObserverProtocol协议。比如下面:

@interface TestViewModel : NSObject<GICObserverProtocol>
@property (nonatomic,copy)NSString *v1;
@property (nonatomic,assign)NSInteger v2;
/// 所有可以被JS调用的方法,都必须带有一个参数,否则无法调用
-(void)onButtonClicked:(id)param;
@end
  1. 初始化ViewModel

     testViewModel = [[[TestViewModel alloc] init] gic_observer];
  2. 绑定按钮的点击事件。

     [btn gic_addEventBinding:[GICEventBinding bindingWidthDataSource:testViewModel expression:@"$.onButtonClicked(`${$.v1}----${$.v2}`)"] forControlEvents:UIControlEventTouchUpInside];

    从这里你可以看到,当按钮点击事件触发后,就会执行JS脚本

    $.onButtonClicked(`${$.v1}----${$.v2}`)

    从这里可以看到,是调用的testViewModel中的onButtonClicked方法,并且传入字符串参数。

  3. 绑定事件中直接设置数据源的value

     [btn gic_addEventBinding:[GICEventBinding bindingWidthDataSource:testViewModel expression:@"$.v2++"] forControlEvents:UIControlEventTouchDown];

    这里每当点击事件触发后,就会对数据源的v2属性+1。如果有其他地方绑定了v2属性,那么也会同时更新数据绑定

  4. 事件绑定直接调用本地代码。

    [btn gic_addEventWatch:^(NSArray * _Nullable params) {
       NSLog(@"点击了");
    } forControlEvents:UIControlEventTouchUpInside];

    这里实际上已经不算事件绑定了,可以说是事件代理。GICDataBinding基于NSProxy实现了一套可以直接block回调按钮事件的库。现在你无需RAC就能实现同样的功能。事实上,你可以对任意对象的任意方法进行watch,在某些情况下,你甚至无需方法交换就能实现类似方法交换的功能,而且这种基于NSProxy实现调用拦截的过程是安全、不冲突的,不会出现方法交换可能引起的崩溃问题(多次交换)。

注意:

所有可以在事件绑定中被调用的ViewModel中定义的方法,必须带有一个参数,哪怕这个参数你不会用到,而且目前只支持带有一个参数的方法。如果你需要传入多个参数,目前唯一的方法是以数组的方式传入

说明

不管是数据绑定还是事件绑定,因为都是基于JS表达式来实现的,所以想要熟练地将数据绑定应用到项目中,还需要您对JavaScript有一定的了解。另外GICDataBinding的JS引擎是基于JavaScriptCore来实现的,所以如果你想在项目中扩展JSCore或者想要以前端开发一样,直接在JS中创建ViewModel,那么需要你对JavaScriptCore有一定的了解。

应用

基于GICDataBinding数据绑定系统,你可以实现一些以前比较复杂的功能。比如:

  1. app主题(Example 有例子)

    可以直接基于绑定系统,将一些主题元素绑定到提供主题数据的模型。这样当用户修改主题时,app可以做出实时的改变。

  2. 重新思考ViewModel的定义。将ViewModel JS化

    你现在可以把部分或全部的原ViewModel移入JSCore,然后通过数据绑定系统直接调用

  3. 配合Texture实现整个UI基于绑定系统的响应式设计。

  4. HotFix

    由于数据绑定和事件绑定都是采用JS表达式的方式调用,所以理论上可以直接通过下发JS脚本来动态修复、增加功能。当然,这仅仅是理论上,实际上实现起来还是有很大开发量的。

  5. 更多功能可自行发掘

作者

龚海伟, [email protected]

许可协议

GICDataBinding可在MIT许可协议下使用。有关更多信息,请参阅LICENSE文件。