LoginSignup
216
214

More than 5 years have passed since last update.

SwiftでJavaScript

Last updated at Posted at 2014-07-10

案ずるより書くが易し…

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

216
214
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
216
214