写给 iOS 程序员的 Weex 教程(6):本地图片加载和上传

由于 Weex 运行在 JavasSriptCore 环境里,它无法直接获取和处理本地图片。所以我们需要有一个变通的方式来实现。
涉及本地图片的操作主要有两种:展示和上传到服务器,下面就这两方面分别说明。(什么?你说编辑?建议你还是放过 weex 吧,用本地代码又快又省事。)

显示本地图片

weex 会把网络图片的请求转发到我们支持 WXImgLoaderProtocol 的对象上。当需要请求图片的时候,会调用此方法:

- (id<WXImageOperationProtocol>)downloadImageWithURL:(NSString *)url imageFrame:(CGRect)imageFrame userInfo:(NSDictionary *)options completed:(void(^)(UIImage *image,  NSError *error, BOOL finished))completedBlock;

这个方法接受一些参数,异步加载图片后返回一个 UIImage 对象。这里就是我们用来支持本地图片的地方。我们可以约定一个规则,通过 url 参数来告诉我们需要加载哪个图片。这里,我建议不要定义特殊协议的 url。因为不利于代码在游览器环境的复用。

我们的方案是把本地图片放一份在网上,然后通过在图片地址后面加参数的形式识别图片在本地对应的名字,类似这样:

http://bjimg5.appdao.com/image/936167445?local=backIcon

参数 local 用来表明是本地图片,值是对应的文件名。拿到文件名后就可以用 imageNamed: 方法获得。
这个实现方式有两个好处:

  1. 游览器端不用修改代码就能直接用;
  2. 如果万一忘记添加图片到工程里,也是能展示图片;

不过需要注意,imageNamed: 方法默认获取的是 mainBundle 里的图片。如果将来通过热更新,添加了新图片,那这些图片是不在 mainBundle 里。直接通过文件名是找不到的。此时需要使用图片基于 mainBundle 的相对路径来:

+ (UIImage*)ark_imageFromName:(NSString*)name
{
    UIImage *img = [UIImage imageNamed:name];
    if (!img) {
        NSString *mediaDir = /* get media folder */;
        NSString *path = [mediaDir stringByAppendingPathComponent:name];
        path = [self relativePathToMainBundle:path];
        img = [UIImage imageNamed:path];
    }
    
    return img;
}

+ (NSString*)relativePathToMainBundle:(NSString*)path
{
    NSString *mainBundlePath = [[NSBundle mainBundle] bundlePath];
    NSString *appDirectory = [mainBundlePath stringByDeletingLastPathComponent];
    NSString *relativePath = [path stringByReplacingOccurrencesOfString:appDirectory withString:@".."];
    return relativePath;
}

通过这样的方式,就能通过文件名同时兼容工程里的图片和某个目录下的图片。

上传图片

Weex 是不能直接操作 iOS 系统里的文件,也就无法直接上传文件。网上有些方案是通过把图片改成 Base64 编码的字符串,然后传递字符串到 weex。这个方案的缺点是如果图片如果较大,对性能和内存都是很大消耗。
我的建议是直接在本地代码里写好上传,然后把上传后的网络地址等数据返回给 weex。这样的好处是能做到后台上传,也能充分利用 iOS 平台的性能。

上传图片的具体方法很简单,各家也不同,我就不具体写了。我就说两个在这个过程中会遇到的问题。

选择图片

这是大家会遇到的第一个问题。Weex 没有封装好系统照片选择器的控件。我的方案是自定义一个 module,然后通过 module 弹出系统的图片选择控件或自己的图片控件。

用户选择完图片后,将图片另存一份到自己的目录。然后构造 file 协议的图片地址,返回给 weex。Weex 拿到这个地址后去显示在界面上。

BOOL success = [imageData writeToFile:path atomically:YES];
if (success) {
    callback([NSURL fileURLWithPath:path].absoluteString);
}

这里还没有完。要真的显示出来,还需要改写一下刚才提到的 WXImgLoaderProtocol 对象的 downloadImageWithURL:imageFrame:userInfo:completed: 方法。需要在这个方法里识别 file 协议的地址,然后用 [UIImage imageWithContentsOfFile:] 返回图片对象。

通过这两步,就完成了图片的选择和显示。

上传进度显示

Weex 的 module 协议只有一个回调,也只能用一次。这就没法做到持续的更新上传文件的进度。这里有一个丑陋点的变通方案。

方案很简单,直接通过 Weex 提供的 globalEvent 模块,持续给 weex 代码发通知。Weex 里就监听对应的事件。方法很丑陋,但是现阶段唯一方案。

这个方法也提示我们,如果我们想做一个本地代码持续更新内容到 weex 代码的事情,也是用 globalEvent

结语

本系列文章都结束了。只讲了在开发会遇到的常见问题,希望大家能举一反三,解决自己遇到的其他问题。
Weex 现在的最新版已经开始支持 vuejs,这也是官方推荐方式。自家的语法将成为了历史。我觉着这是好事,Weex 现在可以专注自己的渲染层,做好 Virtual DOM 到本地控件的转换。我的项目现在已经将所有代码迁移到 vuejs 下,基本按照官方文档做就好。有空的话可能会写一下 weex + vuejs 遇到的坑。

2017/3/24 22:36 下午 posted in  iOS

写给 iOS 程序员的 Weex 教程(5):增量更新实现

Weex 的一大特点就是可以热更新,绕过 App Store 的审核。但 Weex 本身没有提供相关工具,需要我们实现。

就热更新本身而言,最简单的方案是下载一个 zip 包,然后解压替换原来的。只是这个方案的缺点是每次都需要下载一个完整的 zip 包,耗时长和浪费用户流量。我们期望的是每次只更新有变化的,这样能最小化下载体积。

那有什么方案能自动化的找出两个版本之间的变化文件,并更新它?
想想似乎会是一个很复杂的方案,文件那么多,还要记住每个版本有哪些文件,再比对,想想就头大。
能不能实现呢?这个也是能实现的,想想 git 怎么做的。

有没有其他方案实现增量更新呢?
我们换个思路想想这个问题。每个版本我们都会打包成一个 zip 文件。现在如果有一个工具能实现以下两个需求就能解决增量更新的问题:

  1. 比较两个 zip 包 A 和 B,并生成从 A 到 B 的差异文件 patch;
  2. 能用 A + patch 的方式生成 B;

现在就有这样一个工具:bsdiff
bsdiff 是一个从二进制层面去比较文件的工具,能比较任何文件。它是开源的,源代码是 c 语言写的,可以很方便集成到 iOS 里。

bsdiff 分两部分:bsdiff 和 bspatch。bsdiff 用来生成 patch,bspatch用来还原。客户端只需要集成 bspatch,然后利用服务端获取 patch + 老版本去生成新版本。核心原理就这么简单。

剩下的就是根据业务需要服务端提供一个接口来告诉客户端什么时候有新版本和 patch 地址。

国内有一个团队做了一个 react native 的增量更新服务 pushy。它的客户端 sdk 这块是开源的:react-native-pushy。他们也是利用 bsdiff 来实现增量更新,大家可以参考一下。

这篇我没贴什么代码,因为原理很简单,讲出来大家就能理解了。下一篇会讲本地图片在 weex 里的使用

2017/3/21 22:32 下午 posted in  iOS

写给 iOS 程序员的 Weex 教程(4):构建自己的工作流,提高研发打包发布效率

Weex 和 iOS 是两套独立的编译环境,如何让这两者很好的配合,是提高我们开发效率的关键。

Webpack 配置

Webpack 是一个可以根据 js 文件之间引用关系把它们打包成一个文件的工具。weex 提供的 weex loader 则使 webpack 能将 we 文件编译成 jsbundle。我们用 weex init 命令初始化工程的时候已经默认生成了 webpack 的配置文件,开箱即用。只是默认的配置文件只能编译一个 we 文件,我们需要改造它支持同时编译多个 we 文件。
这里我们使用的 webpack 版本是 1.x 版。webpack.config.js 是 webpack 的配置文件。从后缀就能看出来,webpack 的配置文件其实是一个 js 模块。webpack 在加载它的时候会运行里面的代码,并获取到配置信息。
既然配置文件是可运行的,那理论上我们可以在里面干任何想干的事。正是利用这一点,我们可以 wepback 打包我们所需的所有 we 文件。
我们先来看下 webpack 如何配置:

require('webpack')
require('weex-loader')

var path = require('path')

module.exports = {
  entry: {
    main: path.join(__dirname, 'src', 'weex-bootstrap.we?entry=true')
  },
  output: {
    path: 'dist',
    filename: '[name].js'
  },
  module: {
    loaders: [
      {
        test: /\.we(\?[^?]+)?$/,
        loaders: ['weex-loader']
      }
    ]
  }
}

配置信息是通过 module.exports 导出对象。配置对象的 entry 属性指定需要打包的入口 js 文件。entry 可以接受一个对象,里面的 value 是入口 js 文件。output 用来指定输出目录和文件名,上面代码里的 name 引用的是 entry 里的 key。moduleloaders 用来指定解析某些文件,这些 loader 是按顺序串形处理。loader 可以根据规则只处理某些文件。
了解了用法后我们就知道,只需要把所有要处理的 we 文件传到 entry 里就行,其他不用改。具体方法就是遍历指定目录,获取所有 we 文件,然后赋值给 entry

var path = require('path');
var fs = require('fs');

//get all we files
var sourcePath = path.join(__dirname, 'src');
var entries = {};
fs.readdirSync(sourcePath)
.forEach(function(file) {
  var filePath = path.join(sourcePath, file);
  var stat = fs.statSync(filePath);
  var extName = path.extname(filePath);
  var baseName = path.basename(filePath,extName);
  if (stat.isFile() && 
    extName === '.we') {
    var name = path.basename(file,extName);
    entries[name] = filePath + '?entry';
  }
});

module.exports = {
  entry: entries,
  output: {
    path: 'dist',
    filename: '[name].js'
  },
  module: {
    loaders: [
      {
        test: /\.we(\?[^?]+)?$/,
        loaders: ['weex-loader']
      }
    ]
  }
}

这里的 pathfs 是 Node.js 提供的组件。 大家可能注意到文件名后面加了 ?entry 参数。它的用途是告诉 weex loader 当前 we 文件是一个入口文件,用来支持页面配置和页面数据
除了配置所需打包文件,我们还可以根据需要增加点小功能,比如正式发布版本我们想压缩 js 代码。Webpack 的 插件 UglifyJsPlugin 可以做到。更近一步,我们可以通过给 webpack 传参数做到只给正式版增加压缩功能。相关代码如下,就是这么简单:

var process = require('process');

var debug = true;
for (var i = 0; i < process.argv.length; i++) {
  if (process.argv[i] === '--release') {
    debug = false;
  }
}

if (!debug) {
  module.exports.plugins = [
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ];
}

到此,算是配置完 webpack,现在开始改进 npm scripts。

npm scripts

npm scripts 是可定制的 npm 命令,用来运行一些常用操作。Weex 默认生成了 4 条,用途见备注:

  "scripts": {
    "build": "webpack", //打包
    "dev": "webpack --watch", //打包并监控文件变化,变化时再次打包
    "serve": "serve -p 8080", //启动一个小型 web 服务,端口是8080
    "test": "echo \"Error: no test specified\" && exit 1"
  }

这些命令太简单了,有一些不足:

  1. 没有清空打包结果的目录,打包结果可能会受上一次的影响;
  2. 每次开发需要手动运行好几个命令:打包监控(dev),启动 web 服务(serve) 和 启动 debugger 调试(weex debug);

针对这些问题,我们需要改进一下。
scripts 的命令都可以看作是在终端里运行的命令,所以我们可以利用终端的所有命令。改进结果如下:

  "scripts": {
    "build": "rm -Rf dist && webpack",
    "dev": "rm -Rf dist && webpack --watch",
    "serve": "serve -p 8080",
    "debug": "npm run dev & npm run serve & weex debug",
    "release": "rm -Rf dist && webpack --release",
    "test": "echo \"Error: no test specified\" && exit 1"
  }

我增加了一条 debug。这个用来开发调试时用。可以看到,都是终端里的命令用法。
对终端命令不熟悉的,我稍微解释一下:

  • rm 是删除命令;
  • && 表示前一条命令成功了就运行后一条;
  • & 表示前后两条命令同时进行。

npm scripts的用法就是在工程目录下,终端里运行: npm run {key}

引入 jsbundle 到 iOS 工程

通用以上两个配置,我们就能一条命令打包完。但如果想把它们作为资源文件一起打包进 iOS 工程里,则还需要把它们手动拖到 xcode 工程里。

只是如果每次打包完都需要手动拖入那会让人很崩溃的。这里我们就需要利用 xcode 的 “folder reference” 方式把 jsbundle 加入到工程里。“folder reference” 是指xcode 工程只保存对一个目录的引用。当编译的时候,会将这个目录下的所有文件都加入到工程里。同时这个目录下的文件结构也不会变,即如果存在子目录,也会保留它。

如果想让最终打包进应用里的目录名和现在的不一样,则需要有一个软链接中转。软链接类似 windows 下的快捷方式,是对一个文件 / 目录的引用。软链接的名字取我们想要的,然后拖这个软链接进入到工程里就行。
建立软链接的命令如下, 源目录在前,后面是软链接的名字:

ln -s ../dist main

Sublime Text 配置

如果你用的编辑器是 Sublime Text 的话,建议装之前提到的插件,并配置 project 文件。
默认的 cmd + t 快速打开命令会搜索当前打开目录下的所有文件,自然包括 node_modules 目录。这会产生大量干扰信息,肯定不是我们想要的。 project 配置文件能定义只包含某些文件夹,或者排除某些文件夹,很是方便:

{
    "folders":
    [
        {
            "path": ".",
            "folder_exclude_patterns": ["dist", "iOS", "node_modules"],
            "file_exclude_patterns": ["index.html", ".gitignore", "*.swp", "*.orig",".git",".gitmodules","npm-debug.log*"]
        }
    ]
}

以上就是我们现阶段的一些配置项,这些配置会让提高我们工作效率。当然建议大家可以根据自己的需要调整,以符合你们的需要。

下一篇将会如何做自己的增量更新

2017/3/18 21:21 下午 posted in  iOS

写给 iOS 程序员的 Weex 教程(3):界面布局和业务逻辑开发

这篇我们将讨论客户端开发的两大块:界面和业务逻辑。这次我会通过做一个简单的应用来讲。
这个应用很简单,展示 github.com 的 trending repo,点击后跳转用网页打开。

we 文件结构

weex 语法的文件都是以 .we 结尾的文件。文件结构分三大部分:templatescriptstyle

//file.we
<template>
</template>

<script>
</script>

<style>
</style>

template 是界面框架,script 是具体业务代码,style 是 CSS 样式部分。

界面:盒模型与 Flexbox

盒模型是指一个 Weex 组件所占空间大小,看下图就能明白:

Weex 的盒模型基于 CSS 盒模型,可以设置 width, height, padding, border 和 margin。
我们平时所设置的宽高代表的是最里面内容区域的大小。需要记住的一点是我们可以单独设置每一个方向的值,这一点可以使我们可以灵活布局。
Weex 只有 Flexbox 一种布局模型(当然你要全部用绝对值布局我也没话说)。
Flexbox 是如何布局呢?我用尝试用大白话翻译一下:

  • Flexbox 将所有组件按一个方向排布(flex-direction),默认垂直方向;
  • 如果组件太多一行/列放不下的话,还可以指定是否折行排列(flex-wrap,当前不支持wrap-reverse);
  • 可以根据规则确定组件相互之间所占空间比例(flex),如果只有一个组件设置了此项,则它占据所有剩余空间;
  • 同时也可以设置当前方向上多余空间如何处理(justify-content);
  • 对于垂直与当前方向(flex-direction)的空间,也能指定对齐方向(align-items);

    以上括号里属性就是 Weex 现在所支持的。

屏幕适配

当前 Weex(0.9.5) 只支持像素这一个单位,并且屏幕宽度被固定成 750 像素。系统会自动根据实际屏幕大小来缩放所有组件。所以在写界面的时候可以不用考虑适配问题。当然如果想实现所有屏幕都显示一样大小的组件也是能做到,只需要自己反向算好缩放就行,可以根据这个公式计算:

var height = {所需尺寸} * 750 / env.deviceWidth

界面实例

了解上面的布局基础知识后,我们就可以来写界面了。界面大概样子如下图:
Simulator_Screen_Shot

主界面是一个列表,重点就在这个 cell 上。布局上我们可以把 cell 分成三个大块:仓库名、描述和底部其他信息。这个三块用默认的竖向排列。这三块里低部这个稍微复杂点,信息较多。这块的特点是两端对齐,自然我们可以把这个分成左右两部分,使用横向布局。大致的布局思路是这样,对照下 cell 的布局源码看就明白了。( icon 我懒得找图,大家意思明白就好)

      <cell style="height: 200px; border-bottom-width: 2px; border-bottom-color: lightgray;">
        <text style="color: #323232; font-size: 30px; margin: 10px">Goodman/goodapp</text>
        <text style="color: #777777; font-size: 24px; lines: 3; text-overflow: ellipsis; margin-left: 10px; margin-right: 10px;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi eu urna et elit interdum finibus at cursus lectus. Proin nisi ipsum, blandit et malesuada quis, congue a velit. </text>
        <div style="flex-direction: row; margin-top: 10px; justify-content:space-between;">
          <div style="flex-direction: row;">
            <image style="width: 30px; height: 30px; background-color: gray; margin-left: 10px; margin-right: 10px;"></image>
            <text style="font-size: 20px; color: #777777; margin-right: 30px">99999</text>
            <image style="width: 30px; height: 30px; background-color: gray; margin-right: 10px"></image>
            <text style="font-size: 20px; color: #777777;">99999</text>
          </div>
          <div style="flex-direction: row;">
            <text style="font-size: 20px; color: #777777; margin-right: 10px">Good Man</text>
            <image style="width: 30px; height: 30px; background-color: gray; margin-right: 10px;"></image>
          </div>
        </div>
      </cell>

组件拆分和通讯

这个界面很简单,布局代码可以全写在一起。只是为了顺便讲一下如何复用界面组件,我们就把刚才写的 cell 单独写成组件。
写复用组件很简单,和正常的 weex 页面是一样的。其他页面想用某个组件时,只需要在 script 部分引入对应的 we 文件就行:

require('./repo-cell.we');

文件名后缀不要忘了,写上 weex 在编译的时候才能正确识别成组件。组件的文件名就是组件被使用的引用名:

<list class="wrapper">
    <repo-cell></repo-cell>
</list>

list 是 weex 提供的内置列表容器组件,底层实现是 UITableView
组件本身和页面一样,有 script 部分,也有完成的生命周期方法,所以页面上能用的,作为组件都能用。

组件之间通讯

事件监听用 $on, 子组件向父组件发事件用 $dispatch,父组件向子组件发事件用 $broadcast,兄弟组件之间可以通过父组件做一个中转。

// child components
this.$dispatch('naviBar.rightItem.click', {});

// parent components
this.$on('naviBar.rightItem.click', function(e){});

//parent to child
this.$broadcast('event',{});

除了事件外,父组件也可以直接给子组件传递数据。子组件需要现在 data 部分声明变量,然后父组件直接可以在布局部分,绑定数据。注意:data部分用驼峰写法,在布局里需要用横线连接写法:

<!--parent-->
<list class="wrapper">
    <repo-cell repo-info="{{info}}"></repo-cell>
</list>
//child
module.exports = {
    data: {
        repoInfo:null,
    },
};

业务逻辑

本 demo 的业务很简单,获取列表数据和展示,并随着用户滑动来加载更多数据。

加载数据

获取数据需要网络通讯,weex 提供了 stream 模块。stream 是一个异步接口,回调的方式返回数据。为了避免 callback hell, 建议大家用 promise。
Promise 是一个异步调用解决方案。它把一个异步过程疯状到一个 promise 对象里。这个 promise 对象保证未来会有结果返回,不论成功或失败。 promise 对象提供了一个 then 方法,接受两个参数:第一个参数是用来异步成功时的回调,第二个参数是失败或异常情况下的回调。通过 then 方法,我们就可以把相关的业务代码都写在一起,结构清晰:

var promise = asyncCall();
promise.then(function(ret){
  //handle data;
}, function(e){
  //handle error;
});

then 方法返回的是一个新的 promise对象,也就是说我们可以继续衔接调用 then:

promise
.then(func, func)
.then(func, func)

promise 对象还提供了一个 catch 方法, 可以用它来代替 then 方法的第二个参数。建议大家用 catch 方法,因为这样写起来更简洁,方便:

promise
.then(func)
.then(func)
.catch(handleError)

Promise 的具体用法可以看一下其他文档,这里就不赘述。

回到刚才 stream 上,stream本身没提供 promise 的支持,我们需要对其做一个简单的封装:

sendRequest: function(url) {
  var stream = require('@weex-module/stream');
  return new Promise(function(resolve, reject) {
    stream.fetch({
      method: 'GET',
      url: url,
      type: 'json'
    }, function(ret) {
      if (ret.ok) {
        resolve(ret.data);
      }
      else {
        reject(ret.data);
      }
    })
  }, function(){});
}

现在我们就可以开始写数据接口了。 GitHub 没有提供 Trending 的接口,我们就用搜索接口代替一下。
我们要看 Objective-C star 最多的 repos,那请求 url 应该是这样:

var url = 'https://api.github.com/search/repositories?q=language:Objective-C&sort=stars&order=desc&page=' + page;

利用刚才写的 sendRequest 方法,我们的获取列表数据的方法就可以写成这样:

loadNexPage: function(page) {
  page = page || 1;
  var url = 'https://api.github.com/search/repositories?q=language:Objective-C&sort=stars&order=desc&page=' + page; 
  var self = this;
  return this.sendRequest(url)
  .then(function(data) {
    self.repoList = self.repoList.concat(data.items);
    self.canLoadMore = data.items.length === 30; // 30 per page by default
  });
},

现在数据获取逻辑写完了。我们开始把数据展示到界面上。weex支持数据绑定,数据产生变化界面也会改变。绑定的语法是两个大括号:

<text>{{content}}</text>

绑定语法也支持 js 表达式,但只支持单个表达式。想做条件判断的话,可以用条件运算符。

首先,我们先把列表绑定到 cell 上。Weex 提供了 repeat 语法用于支持列表展示:

//template
<list class="wrapper">
 <repo-cell repeat="repo in repoList" repo="{{repo}}"></repo-cell>
</list>

//scripts
 module.exports = {
   data: {
     repoList:[],
     //...
   },
   //....
}

通过 cell 组件的 repo 属性,我们将数据传入到了子组件。这样在子组件里,就能直接绑定数据了:

<template>
  <cell style="border-bottom-width: 2px; border-bottom-color: lightgray;">
    <text style="color: #323232; font-size: 30px; margin: 10px">{{repo.full_name}}</text>
    <text style="color: #777777; font-size: 24px; lines: 3; text-overflow: ellipsis; margin-left: 10px; margin-right: 10px;">{{repo.description}}</text>
    <div style="flex-direction: row; margin-top: 10px; justify-content:space-between; margin-bottom: 20px;">
      <div style="flex-direction: row;">
        <image style="width: 30px; height: 30px; background-color: gray; margin-left: 10px; margin-right: 10px;"></image>
        <text style="font-size: 20px; color: #777777; margin-right: 30px">{{repo.stargazers_count}}</text>
        <image style="width: 30px; height: 30px; background-color: gray; margin-right: 10px"></image>
        <text style="font-size: 20px; color: #777777;">{{repo.forks_count}}</text>
      </div>
      <div style="flex-direction: row;">
        <text style="font-size: 20px; color: #777777; margin-right: 10px">{{repo.owner.login}}</text>
        <image style="width: 30px; height: 30px; background-color: gray; margin-right: 10px;" src="{{repo.owner.avatar_url}}"></image>
      </div>
    </div>
  </cell>
</template>

<script>
  module.exports = {
    data: {
      repo: null,
    },
  };
</script>

到这里基本功能都实现了。现在我们开始实现一下下拉刷新和上拉加载更多。

refresh & load more

weex 自带了这两个两个组件,UI可以自定义。用法很简单, 下面这是一个例子:

    <list class="wrapper">
      <refresh onrefresh="refresh" display="{{showRefresh}}" class="refresh">
        <text if="(showRefresh === 'hide')">下拉刷新</text>
        <loading-indicator class="indicator" if="(showRefresh === 'show')"></loading-indicator>
      </refresh>
      <repo-cell repeat="repo in repoList" repo="{{repo}}"></repo-cell>
      <loading onloading="loadMore" display="{{showLoadMore}}" class="refresh" v-if="canLoadMore">
        <text if="(showLoadMore === 'hide')">上拉加载更多</text>
        <loading-indicator class="indicator" if="(showLoadMore === 'show')"></loading-indicator>
      </loading>
    </list>

上面这段模板里用到了 if 语法。这个语法用来控制是否渲染某个组件。不渲染以为着不会处理任何逻辑,可以加快界面刷新。
模板里的 onrefreshonloading是事件绑定语法,当出现相应事件后会调用后面的方法。后面会详细说明,现在只需知道就好。
这里顺便介绍一下计算属性。计算属性的作用是让我们有机会在数据绑定时支持复杂逻辑。计算属性支持 gettersetter 方法:

computed: {
 fullName: {
   get: function() {
     return this.firstName + ' ' + this.lastName
   },
   set: function(v) {
     var s = v.split(' ')
     this.firstName = s[0]
     this.lastName = s[1]
   }
 }
},

页面跳转

最后,还剩下 cell 的点击事件还未完成。cell 在点击后将打开一个新的页面。新页面是个 web view,将直接加载 repo 的 GitHub 网址。
weex组件提供了好些通用事件,这里我们只需要 click 事件。事件的绑定,只需要写 onXXXX="funcName" ,后面是方法名。也可以直接在这里传递数据并调用:

<repo-cell repeat="repo in repoList" repo="{{repo}}" onclick="showRepo(repo)"></repo-cell>

但是,这里 weex 有一个bug,在子组件上无法用 inline 的形式调用,详情见这个 issue。所以这里只能后接方法名:

<repo-cell repeat="repo in repoList" repo="{{repo}}" onclick="showRepo"></repo-cell>

现在可以在 showRepo 方法里处理事件了。默认会传入事件参数,它有事件相关的属性。

 showRepo: function(e) {
   var utils = require('./utils');
   var repoURL = e.target._vm.repo.html_url; //hack
   var url = utils.absoluteURL('web.js?url=' + repoURL, this.$getConfig().bundleUrl);
   var navigator = require('@weex-module/navigator');
   var params = {
     'url': url,
     'animated': 'true',
   };
   navigator.push(params, function(e) {});
 },

weex 提供了 navigaor 模块用于页面跳转,它需要新页面的绝对地址 URL。但我们写页面地址肯定倾向于写相对地址,这样更灵活。这里我就需要一个方法将相对地址转成绝对地址,然后再加载。转换代码我就不贴了,这里只简单引用。
$getConfig() 是 weex 提供获取基本信息的方法。
大家可能注意到跳转到的地址是 .js结尾而不是 .we,这是因为 weex 会把 we 文件编译成 jsbundle。
跳转后的界面我就不具体说了,只是很简单利用了 weex 提供的 webview 功能。

到此就讲了界面和业务逻辑的大致用法,更细节的问题还是需要大家去看官方文档。
下一篇我们将一下如何构建工作流,方便发布版本

demo下载

2017/1/15 20:50 下午 posted in  iOS

写给 iOS 程序员的 Weex 教程(2):打造自己的 iOS host 应用

官方 Playground 应用只适合学习的时候用一用,真正需要开发应用的时候,还是打造自己的 iOS 应用,方便扩展。

建议在上一篇的工程里新建一个 iOS 目录,把 iOS 工程放在这。这样可以统一管理,之后要支持 Android,Web 都可以新建相应的目录,不相互影响。

引入 SDK

我们将使用 CocoaPods 来集成 WeexSDK。如果现在还不会用这个的,可以反省一下了。顺便引入 Weex 的调试工具 WXDevtool。

# 这是我司维护的一个 CocoaPods 镜像源
source 'git://git.coding.net/fannheyward/CocoaPodsSpecs.git'

target 'hello' do
  pod 'WeexSDK'
  pod 'WXDevtool', :configurations => ['Debug'] #只在 debug 时引入
end

我们为 WXDevtool 增加了 configurations 选项,限制只有测试环境有。大家可以根据自己的需要修改。

配置调试工具 WXDevtool 和 SDK

安装完后,打开 AppDelegate 文件。配置很简单,代码如下。
记住,文档说了,WXDevtool 必须先于 WeexSDK 的初始化。

#import <WeexSDK/WeexSDK.h>
#ifdef DEBUG
#import <TBWXDevTool/WXDevTool.h>
#endif

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
#ifdef DEBUG
    //use weex-devtool to start debug server and paste debug url here
    [WXDevTool launchDevToolDebugWithUrl:@"ws://127.0.0.1:8088/debugProxy/native"];
#endif
    //business configuration
    [WXAppConfiguration setAppName:@"hello"];
    //init sdk enviroment
    [WXSDKEngine initSDKEnviroment];
    
    return YES;
}

Weex 界面的 view controller

WeexSDK 自带了一个 WXBaseViewController 用来展示界面(没有暴露出来),只是这个 WXBaseViewController 不支持实时刷新界面调试,而且也不方便自定义。所以我们来是创建一个自己的 view controller。这个建议直接从官方 playground 工程里抄过来,然后再根据自己的需要修改,原因有二:

  1. 官方探过坑。比如现在 WeexSDK 还不支持 HTTP cache;
  2. view controller需要做一些适配的工作,比如转发 viewDidAppearviewDidDisappear 事件,支持实时刷新等。

代码就不贴了。文后会有 demo。
改完后,需要实现一下 WXNavigationProtocol 协议才能利用上刚才写的 view controller。这个协议是用来实现界面跳转的。像 <a> 标签的点击事件 和 navigator 的 push 和 pop 事件,最后都会转发给这个协议的对象。
实现方式也很简单,从 WeexSDK 里把默认实现复制一份出来,改一下生成 view controller 的方法就好。

image handler

WeexSDK 默认没有实现图片加载,需要自己实现 WXImgLoaderProtocol 协议。这个对象会很有用,之后加载本地图片也会需要它。
现在我们先实现最简单的版本, 网络加载图片用 SDWebImage 这个库:

#import <SDWebImage/SDWebImageManager.h>

@implementation ImageHandler

- (id<WXImageOperationProtocol>)downloadImageWithURL:(NSString *)url imageFrame:(CGRect)imageFrame userInfo:(NSDictionary *)options completed:(void (^)(UIImage *, NSError *, BOOL))completedBlock
{
    if (![self isValidString:url]) {
        return nil;
    }
    
    if ([url hasPrefix:@"//"]) {
        url = [@"http:" stringByAppendingString:url];
    }
    
    SDWebImageManager *mgr = [SDWebImageManager sharedManager];

    id op = [mgr downloadImageWithURL:[NSURL URLWithString:url]
                              options:SDWebImageRetryFailed
                             progress:nil
                            completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                                if (completedBlock) {
                                    completedBlock(image, error, finished);
                                }
                            }];
    return (id<WXImageOperationProtocol>)op;
}

- (BOOL)isValidString:(NSString *)str
{
    if (str && [str isKindOfClass:[NSString class]] && [str length] > 0) {
        return YES;
    }
    
    return NO;
}

@end

然后就是把刚才实现的两个的对象注册到 WXSDKEngine

    [WXSDKEngine registerHandler:[ImageHandler new] withProtocol:@protocol(WXImgLoaderProtocol)];
    [WXSDKEngine registerHandler:[NavigationHandler new] withProtocol:@protocol(WXNavigationProtocol)];

debug view controller

到此,基本已经完成了。不过,我们还需要一个 view controller 用来 debug。为什么呢?因为当你调试多页应用,启动 debugger 的时候,页面不能正常刷新,会变白。不确定这是不是一个 bug。而且,如果debug 启动时的问题,是没有时间立即开启 debugger。所以我们需要先启动进入一个 viewController,然后开启 debugger,然后再进入我们的 Weex 页面,进行开发调试。
当然,发布的时候是可以直接进入 Weex 页面。
这个 debug view controller 很简单,只需要有一个按钮,点击跳转到我们的 Weex 页面就行。具体代码我就不贴了,看文末 demo。

到此,一个基本的 iOS host 应用打造完了,虽然没有什么功能。别担心,后面可以轻松扩展。

优化编辑器:SublimeText 插件推荐

如果你选择 SublimeText 来开发的话,推荐几个插件,默认情况下,对 JavaScript 的补全很差:

  1. Package Control, SublimeText 的插件管理器;
  2. SublimeCodeIntel,JavaScript 的自动补全;
  3. All Autocomplete,SublimeText 只根据当前文件内容自动补全,这个可以从所有打开的文件来补全;

好了,本篇的内容都完了。下一篇讲界面布局并做一个简单应用

demo下载

2017/1/12 22:10 下午 posted in  iOS

写给 iOS 程序员的 Weex 教程(1):hello, world

前言

本系列文章所使用的 Weex iOS SDK 是 0.9.4,Xcode版本是 8.2, 系统版本是 macOS 10.12.2。
本系列所有内容是基于 iOS 的环境所写,Android 和 HTML5 没有试过。适合想要接触 Weex 的 iOS 开发者看。当然,基本概念是一样的,我只是没在其他平台去测试。

环境搭建

Weex 有一个可以在线调试编辑网站 dotWe,适合简单试用。正式开发建议安装一下本地开发环境。
Weex 开发依赖 Node.js。可以去官网下载安装,或者通过 Homebrew 安装:

$ brew install node

通过检查版本来确认是否安装成功:

$ node -v #检查 Node 版本
$ npm -v #检查 npm 版本

npm 是 Javascript 的包管理器,我们需要用它来安装 Weex 的开发工具包 weex-toolkit:

$ npm install -g weex-toolkit # -g 表示是安装到全局环境

如果安装缓慢的话,可以用淘宝的镜像 https://npm.taobao.org
可以在终端里运行一下 weex 命令来验证是否安装成功。(现在我安装到的最新版是 1.0.1-alpha)

至此,环境就搭建好了。

第一个工程:Hello, world

我们新建一个文件夹用来存放工程文件,并用 weex init 命令初始化:

$ mkdir hello
$ cd hello
$ weex init

weex init 会询问工程名,默认是文件夹的名字,用默认的就行。结束后会在当前文件夹下生成以下几个文件:

.
├── .gitignore
├── README.md
├── index.html
├── package.json
├── src
│   └── weex-bootstrap.we
└── webpack.config.js

package.json 是用来声明当前工程依赖的 JavaScript 包,里面已经声明了 Weex 会用的一些依赖。这些依赖现在还没安装,需要先用 npm install 命令安装一遍:

$ npm install

和之前装 weex-toolkit 不同,现在不带任何参数。此时它会在当前目录着 package.json 文件,然后按照里面的内容去把依赖安装到当前目录下的 node_modules 文件夹下。

webpack.config.js 文件是 webpack 的配置文件。webpack 是一个 JavaScript 的打包工具,可以文件依赖关系把多个 JavaScript 文件打包成一个。我们将会用它来把 Weex 文件打包成 JS Bundle 格式,现在先不着急管它。

index.html 是在游览器里预览 Weex 程序的入口文件,现在也不用管。

src 目录用来存放源代码,当然也可以用其他目录,只是没必要。 weex-bootstrap.we 文件是一个 demo 文件。现在我们来尝试把它运行起来:

$ weex debug src/weex-bootstrap.we

weex debug 命令是一个调试命令,适合用调试单个文件,它会做 3 件事:

  1. 编译指定文件到 JS Bundle 格式,并开启监听服务,文件有改动会立即重新编译,刷新客户端页面;
  2. 开启调试服务;
  3. 开始一个小型的 Web 服务,方便客户端,游览器访问;

完成之后会自动打开游览器,显示调试页面;页面下半部分显示两个二维码,左边是调试服务地址,右边是刚才编译的 Weex 文件地址。现在可以用手机上的 Playground 应用来扫描这两个。先扫面左边的,连接好调试服务;再扫面右边的,显示 demo 界面。

现在可以用任何一个文本编辑器来编辑 weex-bootstrap.we 文件,免费的推荐用 Sublime Text。可以试着做一些修改并保存,看看手机上的效果。

到此你已经完成了第一个 Weex 程序,虽然没写一行代码😛。

理解 Weex

看到这,大家可能还是一头雾水,不理解如何用 Weex。我就从使用者的角度来说说 Weex。
weex代码在使用前需要经过 weex 编译工具打包成 jsbundle 文件。这个文件内容看过就明白,全是 javascript 代码。那要如何运行这段代码呢?这就需要用到 weex sdk提供的 WXSDKInstance

WXSDKInstance 类代表着当前 weex 的上下文环境。我们可以用它来加载 jsbundle 文件并与它打交道。通过 WXSDKInstance 来指定 weex 所在的容器 view 的大小。也是通过它来将渲染后的 view 放在界面上的指定的位置。

当我们想要告诉 weex 环境里的代码某些事件时,通过 WXSDKInstancefireGlobalEvent:(NSString *)eventName params:(NSDictionary *)params 方法。当 weex 的代码想告诉本地代码一些事情时,则通过自定义的 module 来实现。

以上就是 weex 的基本使用方法。现在只需要理解就好,后面会带着大家一步步深入。

小结

最后,建议大家先熟悉一下 JavaScript 语言,后面基本全都是用它写。这里有几个语言教程,大家自己看吧:

顺便,也了解一下 CSS 和 HTML,暂时不用掌握多深,Weex 界面样式语法只是他们的一个很小的子集。

下一遍将会打造自己的 iOS 应用,用来运行 Weex 文件,方便以后调试。

2017/1/10 22:51 下午 posted in  iOS

我用 Weex 重写了一个应用

我司的一个应用一直有被 Apple 拒的传统,今年的情况更糟糕,已经好几个月没通过了😢。正好最近我的一个项目结束了(这又是另外一个悲伤的故事🙊……),有时间来研究一下热更新技术,绕过 Apple 的审核。

热更新现在大热的两个方案是 React Native 和 Weex。这两个方案的比较文章网上有很多,我就不多赘述了。大家应该已经知道我选择了 Weex(废话,标题都说了=_=),我就说下为什么选这个:

  1. Weex 的语法上手容易,会基本的 html 和 javascript 就能写;
  2. 国人写的大作,支持一下😄;

嘿嘿,就这么简单的理由,是不是有点拍脑袋?
嗯,是的。因为当时对于我来说也看不出哪个更好,所以先简单选一个试试。结果发现……坑好多……

Weex 的优点我就不说了,他们自己天天宣传。我就说说我发现的缺点吧。

Weex 的坑

当前我用的是 0.9.4,Weex项目还在不断的迭代,我下面说的有些问题以后会解决。

不支持页面之间直接相互传递数据

Weex以页面来划分功能,所有接口基本都是为一个页面服务。每个页面之间是相互独立,互相不知道对方,没法直接相互传递数据。
怎么理解这个逻辑?你可以把整个应用想象成一个网站,每个页面都是一个网页。每个单独网页之间当然不能直接传递数据。Weex界面也是这个情况。
为什么这么设计?因为 Weex 支持 H5。H5 页面之间可没发直接传递数据。
当然了,Weex 还是提供了方法来解决这个问题。

如果你有两个页面,A 页面和 B 页面
1. A -> B,使用 getConfig api 和 storage module 传递数据;
2. B -> A,使用 storage module 传递数据。

白话点,就是传递下一个页面,可以把数据拼接点 B 页面的 url。也可以选择把数据序列化成字符串保存到 storage, 返回的前面一个页面,则只能用 storage

你看,好麻烦。

不支持全局对象和变量

意思就是不能声明单例对象。原因嘛上一点都说了。这点就造成每个页面都要自己去获取登陆状态,用户信息等。很浪费。
对客户端还有一点就是,导致没个页面都得有处理推送消息之类的全局性代码……

不支持 z-index

这个主要影响动画,没法做一些手势动画。

不支持 scrollView 的滚动事件

这个同样影响动效实现。当然在 sdk 里提供了客户端代码监听的方法,想在 Weex 的代码里有就只能自己想办法。

不能动态创建 html 标签

严格说,应该说是不能方便的动态创建 html 标签,因为 Weex 提供了底层的 Virtual Dom API。但是呢,你看了就知道,聊胜于无。解决这个问题的办法就是所有界面组件都先写上,然后通过 if 属性来控制显示与否。

文档缺乏

Weex文档写得相当简单,很多东西需要自己去摸索。更新也慢,sdk 里有好些内容文档没有体现。所以遇到问题都我都是去看 iOS sdk的源码来解决……

以上就是写了两个多月的体会到主要问题。虽然问题多,但 Weex 的扩展性还行,遇到解决不了的问题就自己撸一个就好,你看我撸了多少:
custom module

经验分享

既然已经探了这么多坑,我准备写一系列的教程分享出来。我是一个 iOS 开发者,所以主要以 iOS 的角度来写。我不打算写太具体的细节,入门的内容官方文档已经有了,我主要根据开发过程中会遇到的问题来写。
这是一个文章目录:
写给 iOS 程序员的 Weex 教程(1):环境搭建
写给 iOS 程序员的 Weex 教程(2):打造自己的 iOS工程
写给 iOS 程序员的 Weex 教程(3):界面布局和业务逻辑开发
写给 iOS 程序员的 Weex 教程(4):构建自己的工作流,提高研发打包发布效率
写给 iOS 程序员的 Weex 教程(5):增量更新实现
写给 iOS 程序员的 Weex 教程(6):本地图片加载和上传

2017/1/8 17:37 下午 posted in  iOS

利用 podspec 的 subspec 来实现多个预处理宏的灵活配置

什么是预处理宏

开始正题前,我先说下简单什么是预处理宏,因为可能有些人不知道。先上一段例子方便理解:

[JSPatch startWithAppKey:@"YOU_GUESS"];
#ifdef DEBUG
[JSPatch setupDevelopment];
#endif
[JSPatch sync];

代码很简单,对 JSPatch 这个库的初始化。 DEBUG 在这里就是提前定义好的预处理宏。通过结合 #ifdef,可以只在编译测试版的时候设置 JSPatch 为Development状态。当要发布正式版的时候也不用修改代码,直接编译就行,中间那句[JSPatch setupDevelopment];不会出现在代码里。
看明白了吧,预处理宏的主要就是用来有目的的引入或移除一部分功能(代码)。

本文的使用场景

一个第三方库会有很多功能,其中有一部分功能需要在编译阶段就决定是否引入。比如 IDFA,Apple 要求使用的话需要在提交审核的时候声明,不然就被拒。此时如果应用不用,那就会被你拖累。所以需要提供一个方法从代码里删除,这就需要用到预处理宏。用类似上面的方式改好后,让用户在 Build Settings 里设置一下就 OK。
如果这个库支持 CocoaPods,可以建一个 subspec 省去用户手动修改:

s.subspec 'IDFA' do |f|
  f.dependency 'YOUR_SPEC/core'
  f.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => 'ENABLE_IDFA=1'}
end

当有多个预处理宏需要设置,可以都写在这一个里面。
可如果不想写在一起,想让用户自己选择开启某些的话,怎么办?
答案很简单,多写几个 subspec。用户需要哪个,就引入哪个。具体例子继续看。

Subspec 的灵活配置

假设我们有两个功能需要预处理宏来开关,那 podspec 这么写

Pod::Spec.new do |s|

  #设置 podspec 的默认 subspec
  s.default_subspec = 'core'
  #主要 subspec
  s.subspec 'core' do |c|
    c.source_files  = "*.{h,m}"
    c.public_header_files = "*.h"
    c.frameworks = 'UIKit',
    c.libraries = 'icucore', 'sqlite3', 'z'
    c.platform = :ios, "7.0"
  end
  #功能1,引入则开启
  s.subspec 'IDFA' do |f|
    f.dependency 'YOUR_SPEC/core'
    f.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => 'ENABLE_IDFA=1'}
  end

  #功能2,引入则开启
  s.subspec 'IDFB' do |f|
    f.dependency 'YOUR_SPEC/core'
    f.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => 'ENABLE_IDFB=1'}
  end  

end

这里面通过两个 subpec 来开关功能。当用户用的时候,则在 Podfile 里这么引入

pod 'YOUR_SPEC', :subspecs => ['IDFA', 'IDFB']

上面就能开启两个功能。简单吧?

-EOF-

题图: sitesbay.com

2016/11/21 21:37 下午 posted in  iOS

APP 多发的自动化打包方案

最近项目为了尝试一些危险的推广方案和防止App Store下架风险,在不同的帐号下发布了相同的应用。我在这里面做了一套自动化打不同应用的方案,加快发布应用流程。

传统方法

以前,为了做到相同的应用多发,我们在原来的工程里,复制一份Target,然后修改bundle id、icon等信息。然后打包发布。
这个方法的好处是开始很简单,任何人都能解决,但缺点也很明显:

  1. 在 APP 的改进演化过程中,我们新加入文件的时候必须要选上每个Target;
  2. 在发布的时候,需要手动选择每个Target、改证书、打包、上传、登录网站提交审核;

这些步骤很花时间,特别是新加文件这个问题很容忘记,导致打包失败。为了解决这个问题,于是有了下面一整套自动化解决方案

自动化方案简介

此方案的基本原则是首先工程里只需要有一个Target,其次是自动化修改必要文件,然后打包。
为了做到这些,用到了两个工具:

  1. Bash script: 修改bundle id、icon、证书、各种keys;
  2. fastlane: 打包、上传、提交审核;

先说Bash,这就是简单 sed 替换。这里需要提高一个工具 PlistBuddy,这个是Mac自带的命令行修改pist文件超好用工具,特别傻瓜。

fastlane是一套开源的 iOS 和 Android 打包工具包。它干嘛?在 itunesconnect 和 developer站点自动创建应用和证书,上传、打包、更新应用信息和提交审核。很强吧!有一点需要注意:它的工具组件之间是相互独立的,工具之间配合是靠环境变量来沟通,这点很坑。想要灵活衔接,就需要脚本了。

方案说明

在我的工程里,有一个 release.sh,这个就是脚本。还有很多 *.conf 的文件是应用相关信息。我就顺着 release.sh 的内容说下原理。

保存每个应用不同的内容到配置文件

首先我们先讲需要修改的点保存到配置文件里。我的项目里是 *.conf

app_name='' #填入自己的内容
app_bundle_id=''
app_product_name=''
app_scheme=''
app_icon_path=''
qq_id=''
weibo_key=''

获取最新证书

这里用到 fastlane 里的 match 工具,他可以一个自动管理证书和 provisioning profile。

match --git_branch ${apple_id} -y appstore -a ${app_bundle_id} -u ${apple_id} -r ${YOUR_CERTIFICATES_REPO}

读取 *.conf 文件,修改工程

这里除了要修改bundle id、icon、keys,记得还要修改 provisioning profile 到 distribution的。这里修改 provisioning profile 时,我用到了 match 设置的环境变量,这是为了避免去找 profile 的 udid。

#sample
echo "changing ga key to: ${ga_key}"
sed -i '' "s/${OLD_GA_KEY}/${ga_key}/" $appDelegate_file
echo "set app version string:${app_version_string}"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${app_version_string}" $info_fi

gym 打包生成 ipa

这个没什么,一个命令搞定。

gym --scheme "${YOUR_TARGET_SCHEME}" --clean true --configuration "Release" --output_directory ${ipa_path} --use_legacy_build_api true --output_name $ipa_name --include_symbols true

deliver 上传、发布;

deliver除了上传外,还能修改应用的所有 metadata 信息,并提交。metadata按照指定文件格式放好就行。提交的话,因为需要一些参数,这个我先自动生成配置文件,然后再用deliver,最后删掉。

echo ">> create Deliverfile"
echo "submission_information({
add_id_info_uses_idfa:true,
add_id_info_serves_ads:true,
add_id_info_limits_tracking:true,
export_compliance_uses_encryption:false,
export_compliance_encryption_updated:false})" > Deliverfile
echo ">> deliver ipa"
deliver -u ${apple_id} -a ${app_bundle_id} -i ${ipa_path}${ipa_name}.ipa -z ${app_version_string} --submit_for_review -f

恢复到最初,打包下一个

这里,我在开始修改文件前都复制了一份备份文件,这时候就是还原场景,避免干扰下一次打包。

cp ${info_file}.bp $info_file 
rm ${info_file}.bp
...

以上是大概内容。可以到这里看我的 release.sh demo,这个 demo 需要改些地方适合你的工程,不能直接使用。大家可以直接复制过去修改一下。这里面我填了很多坑,避免大家再来一遍。

2016/6/8 23:5 下午 posted in  iOS

iOS Theme 开发续:Theme 的 Live Update

上一篇讲了Theme开发的思路,这次总结一下实时更新Theme时会遇到的问题。

先说下Live Updat的方法。很简单,只需要有Theme变化的组件订阅一个Theme变化的notificaiton,然后在收到notification后读取最新的Theme资源就行。为了简化Theme变化的代码,可以自定义控件,把Theme相关代码都封装好。之后用起来会很方便些。

其次,再说说Live Update要注意的问题:

  1. 最基础也是最主要的一点:一定要确保在主线程里修改UI相关内容。Notification的接收者在被调用的时候的线程是同发送的时候所在线程一样。即如果发送的时候不是主线程,则接收者被调用的时候不在主线程;

  2. 如果使用UIAppearance协议来修改Theme,记得改完后把View从window里移出,然后移入。因为在已经在window里的view不会立即更新。参考 Apple文档

    iOS applies appearance changes when a view enters a window, it doesn’t change the appearance of a view that’s already in a window. To change the appearance of a view that’s currently in a window, remove the view from the view hierarchy and then put it back.

  3. UITableViewCell的Theme更新不能[tableView reloadData]的方法。因为 tableView 会复用已生成的。所以cell需要自己更新Theme资源;

以上是我的一些总结,欢迎讨论拍砖

2015/3/18 23:17 下午 posted in  iOS