用JSON 写iOS UI 原理说明

1,750 阅读3分钟

近日写了一个好玩的库JSONRnederKit
大概整个人处于空窗期吧,闲不下来,同时最近经历了一些事情,就让写代码来填充自己。
每次因为需求更新app,时间都非常长。比如说某个节日,想做些彩蛋,你可能就要更新版本了。为了解决这个痛点,突发奇想,能不能用JSON 做一些简单的单页应用呢,事实上是完全可以的。

截图如下

效果图
效果图

核心文件

SSJSContext.m,SSBaseRenderController.m,NSObject+SSRende.m,SSKit.js,

文件 作用
SSJSContext.m 负责接收JSON 生成新的容器View 返回给外界使用
SSBaseRenderController.m 接收SSJSContext 返回的容器视图并显示
NSObject+SSRende.m 一共一个方法,调用OC 对象的任何方法
SSKit.js 仿照UIKit,实现JS 的数据结构
SSTool.js 提供字符串解析帮助

流程图

st=>start: 获取JSON 
e=>end: 显示结束
op1=>operation: SSJSContext提供JSON和wrapperView给JS
op2=>operation: JS 接收JSON 开始解析
op3=>operation: 解析完毕,JS调用OC 生成视图并设置各种属性
op4=>operation: 设置完毕,通知jsContext,并返回wrapperView 
op4=>operation: renderController 接收wrapperView

st->op1->op2->op3->op4->e

如何解析JSON

这里面肯定少不了OC和JS交互的,为了方便交互,我在SSJSContextJS 定义了oc_invokeWithArgs(ocObj,method,args);这样JS可以调用任意OC对象的任意方法,同时定义了一系列和OC 对应的类,继承关系也对应,例如:
NSObject->NSObject,
Controller->UIViewController,
View->UIView
...

//JS调用该函数可以达到调用OC实例对象的方法,其中JS传递的参数会自动转化为OC相应的类型
self[@"oc_invokeWithArgs"] = ^id(JSValue *ocPointer,
                            NSString *methodName,
                            JSValue *args){
   id ocObj           = [ocPointer toObject];
   SEL methodSelector = NSSelectorFromString(methodName);
   NSArray *oc_args   = [args toArray];
   //调用给NSObject添加的方法,可以调用OC实例对象的方法
   id obj             = [ocObj js_performSelector:methodSelector withObjects:oc_args];
   return obj;
};
class NSObject{
    constructor(){
        //保存OC对象,相当于强引用了指针
        this.ocPointer = null;
        //保存OC对象的类名,用于给OC反射创建一个OC实例
        this.ocClsName = 'NSObject';
    }
    ...
    
    //创建OC对象
    creatNative(){
        //调用OC方法,创建实例,并保存
        this.ocPointer = oc_creatObject(this.ocClsName.firstUpperCase());
    }
    ...
    
    //将JS对象绑定到OC对象
    bindJSValueToOC(){
        ...
        //执行OC实例对象的相应方法
        oc_invokeWithOneJSArg(this.ocPointer,'setJsValue:',this);
    }
    
    //调用OC
    invokeNative(method,...args){
        return oc_invokeWithArgs(this.ocPointer,method,args);
    }
}

视图生成和层次关系解决

JS 里面接受JSON 传递给controller 实例,调用controllerproduceSubviews 方法

    produceSubviews(){
        //this.components 就是获取的JSON里面的components
        if(!this.components) return;
        this.components.forEach((item,index)=>{
            //这里把ListView反射,生成ListView实例
            let view = eval(`new ${item.type}()`);
            //把单个单个component(item)交给view
            view.initWithJSON(item);
            this.wrapperView.addSubview(view);
            this.viewStore.set(view.ocIdentify,view);
        });
    }

走到view.initWithJSON(item);的时候,view首先根据item这个对象设置好自己的属性,例如ocClsName等,再然后调用view.creatNative创建一个OC对象,并自己保存在this.ocPointer,其次再遍历item里面的components创建子视图,这就是一个递归调用,这样就解决了视图的层级关系。再添加子视图this.addSubview(view),这个函数会调用OC的方法。这样就已经布局好了视图。

生成并布局好了视图后,JSwrapperView交给SSBaseRenderController来进行显示。


JSON字符串中的变量和函数处理

主要得益于ES6的模板字符串的设计
我怎么把 "`${UI.screenW}`" 变成"375"呢,解决方法可能方法比较笨。

let string = "`${UI.screenW}`";
string = "return" + string;
value = (new Function(string)).call();

这样 "`${UI.screenW}`" 就变成了"375"

这样函数也不难处理了仍然是通过改字符串并new Function(string)来解决。
由于JSON传递给JS后就直接变成了一个对象,这样可以很容易对变量来进行操作,也为数据流动的实现提供了可能。


数据的流动问题

难的是怎样设计数据流动的形式,我琢磨了很久。 最后决定使用“执行Action”的形式来解决数据流动,参考了一下Redux。把视图变成一个状态机,由状态来决定视图上面显示的东西。

里面有很多细节处理,可以看源码,有详细的注释,这里只是大致说一下原理。


联系我

无论是否有疑问欢迎和我一起讨论没我会迅速回复你
地址:feelings0811@wutnew.net 或者 github.com/cx478815108