本片文章翻译自raywenderlich
虽然看起来 Swift
和 JavaScript
看起来有很大的不同,但你可以使用二者创建一个灵活的 iOSAPP
在这篇 JavaScriptCore
教程中,你将建立一个网页配套的 iOSAPP
复用存在的 JavaScript
代码,通过本教程,你将学到
- JavaScriptCore framework
- 在
iOS
中怎样调用JavaScript
代码 - 在
JavaScript
中怎样调用iOS
的代码
你不必对JavaScript编程很有经验,如果这篇教程激起你学习JavaScript的兴趣,你可以看 Mozilla Developer Network的入门教程 不想看英文的话,
注 :廖雪峰的博客作为入门也是一个不错的选择,或者买这本书

0X0 开始
点击这里下载本篇教程所用到的代码,解压后,你将得到以下目录
- Web目录,包含
HTML
和CSS
文件 - Native目录,iOS工程目录,本篇教程主要在这个目录下进行
- js目录,这篇教程所用到的JavaScript代码
这个 APP
叫做 ShowTime
你可以输入价格,从 iTuns
中搜索电影,你可以打开在浏览器中打开 Web/index.html,然后输入价格,回车后,你将看到这个页面所呈现的内容;

在 iOS
端,打开工程,编译后,你将看到如下界面

0X2 调用JavaScript方法
回到 Xcode ,展开 Data 文件夹,打开 MovieService.swift ,这个类将请求并处理从 iTunes 返回的数据,现在,他们大部分是空的,我们的工作就是把这些方法实现了。
通常,MovieService 的工作流将是这样的
-
loadMoviesWithLimit(_:onComplete:)
取得对应的电影数据 -
parseResponse(_:withLimit:)
将借助于JavaScript
代码来处理请求回来的数据。
第一步是获取电影列表,如果你熟悉 JavaScript
编程的话,一般我们使用 XMLHttpRequest
对象来进行网络请求。由于这个对象并不是 JavaScript
语言本身的对象,如果使用了这个对象,那我们将无法再 iOS APP 中的上下文中使用,所以,我们还是要用 native 来进行网络请求的
在 MovieService
类中,找到 loadMoviesWithLimit(_:onComplete:)
方法,改成下边这样
func loadMoviesWithLimit(limit: Double, onComplete complete: [Movie] -> ()) {
guard let url = NSURL(string: movieUrl) else {
print("Invalid url format: \(movieUrl)")
return
}
NSURLSession.sharedSession().dataTaskWithURL(url) { data, _, _ in
guard let data = data,
jsonString = String(data: data, encoding: NSUTF8StringEncoding) else {
print("Error while parsing the response data.")
return
}
let movies = self.parseResponse(jsonString, withLimit:limit)
complete(movies)
}.resume()
}
这一段是用 NSURLSession
来获取电影列表,在把网络请求的响应信息传递个 JavaScript
代码前,你要有一个 JavaScript
可执行的上下文,首先,在 MovieService.swift
中加入下边的代码来导入 JavaScriptCore
import JavaScriptCore
然后在 MovieService
中定义如下属性
lazy var context: JSContext? = {
let context = JSContext()
// 1
guard let
commonJSPath = NSBundle.mainBundle().pathForResource("common", ofType: "js") else {
print("Unable to read resource files.")
return nil
}
// 2
do {
let common = try String(contentsOfFile: commonJSPath, encoding: NSUTF8StringEncoding)
context.evaluateScript(common)
} catch (let error) {
print("Error while processing script file: \(error)")
}
return context
}()
这样就定义了一个懒加载的 JSContext
属性
- 加载了
common.js
, 这里边包含了你想要访问的JavaScript
代码 - 加载 js 之后,
context
通过执行context.evaluateScript()
来访问js的内容。传递的参数就是js文件的内容。
是时候来执行 JavaScript
方法了,还是在 MovieService.swift
这个类里边,找到 parseResponse(_:withLimit:)
函数,添加如下代码
func parseResponse(response: String, withLimit limit: Double) -> [Movie] {
// 1
guard let context = context else {
print("JSContext not found.")
return []
}
// 2
let parseFunction = context.objectForKeyedSubscript("parseJson")
let parsed = parseFunction.callWithArguments([response]).toArray()
// 3
let filterFunction = context.objectForKeyedSubscript("filterByLimit")
let filtered = filterFunction.callWithArguments([parsed, limit]).toArray()
// 4
return []
}
我们来一步一步的看一下
- 首先,确保
context
属性被正确的初始化,如果在初始化的时候发生了错误,也就没有必要在执行下去了,比如说common.js
不在bundle
中。 - 你询问
context
对象来提供一个parseJson
方法,就像先前提到的一样,查询的结果被包含到一个JSValue
对象中,下边你将通过调用callWithArguments(_:)
来执行JavaScript
方法,传递一个数组过去,最后,再把JSValue
对象转为数组。 -
filterByLimit()
返回那些适合给定价格的电影列表。 - 现在已经获得了电影列表。但是这儿还缺失了一块代码,
filtered
持有一个数组,我们应当把他映射为本地的Movie
类型。
你可能发现在这里用
objectForKeyedSubscript()
有点古怪,很不幸,Swift
只能访问这些原始的方法,而不能把他们转为适当的脚本方法。但OC
却可以使用方括号语法来来使用下标访问。
暴露 Native 代码
在 JavaScript
中运行 Native
代码的方法就是定义 block
,他们将会被自动桥接到 JavaScript
方法中。 但有个小问题,这种方式只对 OC
有效,对 Swift
的闭包无效。为了执行闭包,你要执行以下两步
- 使用
@convention(block)
把Swift
闭包转为OC
的block
。 - 在你映射到
JavaScript
方法之前,应该转为AnyObject
。
在 Movie.swift
添加 下边的代码
static let movieBuilder: @convention(block) [[String : String]] -> [Movie] = { object in
return object.map { dict in
guard let
title = dict["title"],
price = dict["price"],
imageUrl = dict["imageUrl"] else {
print("unable to parse Movie objects.")
fatalError()
}
return Movie(title: title, price: price, imageUrl: imageUrl)
}
}
这个闭包传递一个 JavaScript
数组(用字典代替),并且用它来构建 Movie
实例。
回到 MovieService.swift
在 parseResponse(_:withLimit:)
中,用一下代码替换 return
这一段
// 1
let builderBlock = unsafeBitCast(Movie.movieBuilder, AnyObject.self)
// 2
context.setObject(builderBlock, forKeyedSubscript: "movieBuilder")
let builder = context.evaluateScript("movieBuilder")
// 3
guard let unwrappedFiltered = filtered,
let movies = builder.callWithArguments([unwrappedFiltered]).toArray() as? [Movie] else {
print("Error while processing movies.")
return []
}
return movies
- 使用
Swift
的unsafeBitCast(_:_:)
方法把一个block
转为一个AnyObject
- 调用
context
的setObject(_:forKeyedSubscript:)
方法把block
加载到JavaScript
的运行时,然后,使用evaluateScript()
得到block
在JavaScript
中的引用。 - 最后一步是通过
callWithArguments(_:)
执行JavaScript
中的block
,传一个 JSValue 的数组作为参数。返回的参数将是一个包含Movie
对象的数组。
是时候看看你的代码的效果了。编译并运行,输入价格之后回车,你将看到如下界面。

恭喜你,现在已经创建了一个可以浏览电影的超棒 APP
,并且重用了用不同语言编写的已经存在的代码。

你可以在这里下载本教程完整的代码。
如果你想学习更多的关于 JavaScriptCore
的内容, 请参看 WWDC 2013 Session 615
如有翻译不足的地方,还望多多指正,谢谢!!!
网友评论