案ずるより書くが易し…
Swiftのウリの一つは(Objective-)?Cに直アクセスできることですが、それが意味するのは、JavaScriptに直アクセスできるということでもあったのです。
とりあえずJS使うだけなら
こんだけ。
import JavaScriptCore
let ctx = JSContext()
let ary = [0, 1, 2, 3]
var jsv = ctx.evaluateScript(
"\(ary).map(function(n){return n*n})"
)
println(jsv)
var a = jsv.toArray()
println(a)
はい。見てのとおり、import JavaScriptCore
して、JSContext()
でJSの実行環境をこしらえて、それに.evaluateScript()
でString
食わせれば、おしまい。
実行結果はJSValue
という型で、見ての通りObjective-Cに対応する型へ変更するメソッドもついてます。
JSにSwiftの値を渡すには?
しかしこれだけではつまらない。Swiftの値をいちいち文字列化して.evaluateScript()
のは面倒そう。直接JSの変数にアクセスすることはできないでしょうか?
そう。Objective-Cならね。
ctx[@"theAnswer"] = @42;
これが出来るなら、当然Swiftでも
ctx["theAnswer"] = 42
とかできそうなのですが、残念ながらObjective-Cのsubscriptまでは実装されていなかったようです。
というわけでbridgeを書きます。ここではプロジェクト名がjs
になっていますが、実際に使う際には各自読み替えてください。
js-Bridging-Header.h
#import <JavaScriptCore/JavaScriptCore.h>
JSValue *getJSVinJSC(JSContext *ctx, NSString *key);
void setJSVinJSC(JSContext *ctx, NSString *key, id val);
jsc.m
#import <JavaScriptCore/JavaScriptCore.h>
JSValue *getJSVinJSC(JSContext *ctx, NSString *key) {
return ctx[key];
}
void setJSVinJSC(JSContext *ctx, NSString *key, id val) {
ctx[key] = val;
}
あとは、
main.swift
extension JSContext {
func get(key:NSString) -> JSValue {
return getJSVinJSC(self, key)
}
func set(key:NSString, _ val:AnyObject) {
setJSVinJSC(self, key, val)
}
}
とSwiftでしておけば、
ctx.set("foo", [1,2,3])
jsv = ctx.evaluateScript("bar = foo.map(function(n){return n*n})")
println(ctx.get("bar")) // [1, 4, 9]
とあいなります。めでたしめでたし…?
JSにSwiftのクロージャを渡すには?
しかし残念ながら、Objective-Cのid
、SwiftのAnyObject
は、関数までは扱ってくれないのです。
ctx.set("add", { (x:Double,y:Double) in x + y }) // error
「(Double,Double)->DoubleはAnyObjectではありませんっ」って怒られちゃいます。
さあ、どうする?
結局こうしました。ちょっと泥臭い方法ですが、ほとんどのシーンでこれで間に合うはずです。
js-Bridging-Header.h に追加
void setB0JSVinJSC(JSContext *ctx, NSString *key,id(^block)());
void setB1JSVinJSC(JSContext *ctx, NSString *key,id(^block)(id));
void setB2JSVinJSC(JSContext *ctx, NSString *key,id(^block)(id,id));
jsc.m に追加
void setB0JSVinJSC(
JSContext *ctx, NSString *key, id (^block)()
) {
ctx[key] = block;
}
void setB1JSVinJSC(
JSContext *ctx, NSString *key, id (^block)(id)
) {
ctx[key] = block;
}
void setB2JSVinJSC(
JSContext *ctx, NSString *key, id (^block)(id, id)
){
ctx[key] = block;
}
Swiftに追加
extension JSContext {
func set(key:NSString, _ blk:(()->AnyObject!)?) {
setB0JSVinJSC(self, key, blk)
}
func set(key:NSString, _ blk:((AnyObject!)->AnyObject!)?) {
setB1JSVinJSC(self, key, blk)
}
func set(key:NSString, _ blk:((AnyObject!,AnyObject!)->AnyObject!)?) {
setB2JSVinJSC(self, key, blk)
}
}
.set
が複数あることに着目。こうすることで、単一のメソッドで通常の値も関数も扱えます。
要するに、引数が0,1,2個の場合にそれぞれbridgeするというわけです。
// block w/ no argument
ctx.set("hello") { ()->AnyObject! in
return "Hello, JS! I am Swift."
}
println(ctx.evaluateScript("hello"))
println(ctx.evaluateScript("hello()"))
// block w/ 1 argument
ctx.set("square") { (o:AnyObject!)->AnyObject! in
if let x = o as? Double {
return x * x
}
return nil
}
println(ctx.evaluateScript("square"))
println(ctx.evaluateScript("square()"))
println(ctx.evaluateScript("square(6)"))
// block w/ 2 arguments
ctx.set("multiply") { (o:AnyObject!, p:AnyObject!)->AnyObject! in
if let x = o as? Double {
if let y = p as? Double {
return x * y
}
}
return nil
}
println(ctx.evaluateScript("multiply"))
println(ctx.evaluateScript("multiply()"))
println(ctx.evaluateScript("multiply(6)"))
println(ctx.evaluateScript("multiply(6,7)"))
見ての通りAnyObjectゆえ型チェックと変換の手間は避けられませんが、それを差し引いてもあっけにとられるほど簡単です。
三つ以上の値を渡したいなら、Array
を経由した方が楽でしょう。
// for any more arguments, just use array instead
ctx.set("sum") { (o:AnyObject!)->AnyObject! in
if let a = o as? NSArray {
// since the NSArray elements are also AnyObject,
// we can't reduce()
var result = 0.0
for v in a {
if let n = v as? Double {
result += n
} else {
return nil
}
}
return result
}
return nil
}
println(ctx.evaluateScript("sum([0,1,2,3,4,5,6,7,8,9])"))
以上の結果は、Githubにもおいときます。
もっとエレガントな方法を見つけたという方は、ぜひご報告を。
Enjoy!
Dan the Polyglot