iOS--GICDataBinding | 基于NSProxy开发的数据绑定库--支持JS表达式

831 阅读6分钟

简介

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那样的自动根据数组内容变更,从而update UI的功能。

  5. 支持事件绑定重点

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

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

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

安装

pod 'GICDataBinding'

github地址:github.com/ghwghw4/GIC…

使用方法

数据模型

所有的数据绑定功能的前提是一个可以被观察的对象,就像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实现调用拦截的过程是安全、不冲突的,不会出现方法交换可能引起的crash问题(多次交换)。

注意:

所有可以在事件绑定中被调用的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脚本来动态修复、增加功能的。当然,这仅仅是理论上,真要实现起来还是有很大开发量的