110 Views
August 09, 14
スライド概要
Objective-C や Swift のネイティブコードから JavaScript をランタイムで実行するための JavaScriptCore.framework のお話です。基本的な機能の説明と、注意点を整理して紹介しています。
※ Docswell での公開に移行する直前の Slideshare での閲覧数は 38,607 でした。
正統派趣味人プログラマー。プログラミングとは幼馴染です。
JavaScriptCore framework の普通な使い⽅方 EZ-‐‑‒NET 熊⾕谷友宏 @es_̲kumagai http://program.station.ez-‐‑‒net.jp/
⾃自⼰己紹介 EZ-‐‑‒NET 熊⾕谷友宏 http://program.station.ez-‐‑‒net.jp/ @es_̲kumagai EZ-‐‑‒NET IP Phone ⾳音で再配達ゴッド ⾳音で再配達 ⾳音でダイヤル いつもの電卓 for iPad いつもの電卓 for iPhone
書籍 • • • • • • • • • Xcode 全機能を網羅羅 プロジェクトの作り⽅方 ソースコード編集の効率率率化 ショートカットキーの紹介 オートレイアウトの使い⽅方 ローカライズの設定⽅方法 バージョン管理理の使い⽅方 ビルド設定とスキーム設定 ほか、とにかくいろいろ こんなご時世ですが ぜひ⼿手に取ってパラパラめくってみてください。 特設サイト ̶— http://ez-‐‑‒net.jp/sp/xcode5/
JavaScriptCore.framework
特徴 JavaScript ⾔言語 1. Web でお馴染みのスクリプト⾔言語 2. ⼿手軽にコードを組み⽴立立てられる 3. JavaScript を使える⼈人は多いはず
特徴 JavaScriptCore.framework 1. アプリ内で JavaScript を実⾏行行可能 2. ネイティブコードとの相互運⽤用が可能 OS X 10.9、iOS 7.0 から利利⽤用可能
特徴 1. アプリ内で JavaScript を実⾏行行可能 1. スクリプトをテキストで⽤用意する 2. 実⾏行行する直前までに⽤用意すれば良良い 3. ビルドに依らない実装が可能になる ➡ コードを⾃自由に差し替えられる ➡ リソースと同じように DL 適⽤用できる ➡ アプリ使⽤用者にカスタムスクリプトを 書かせる機能を容易易に実現できる
特徴 2. ネイティブコードとの相互運⽤用 1. 変数の値を⾃自由に受け渡しできる ➡ ネイティブの⾃自作クラスも交換可能 2. JavaScript からネイティブコードの メソッドを実⾏行行できる 3. ネイティブコードから JavaScript の関数を実⾏行行できる
JavaScriptCore.framework 実⾏行行の流流れ
実⾏行行⼿手順 (1/4) JavaScriptCore をインポート import JavaScriptCore ターゲット設定の Linked Frameworks and Libraries で JavaScriptCore.framework をリンクしておくこと
実⾏行行⼿手順 (2/4) 実⾏行行環境のコンテキストを⽣生成 let context = JSContext() このコンテキスト内で JavaScript を実⾏行行する
実⾏行行⼿手順 (3/4) JavaScript コードを実⾏行行 let script = "var value = encodeURI('<name>');" context.evaluateScript(script) 実⾏行行結果はコンテキスト内に蓄積される
実⾏行行⼿手順 (4/4) コンテキストから値を取得 let value:JSValue = context.objectForKeyedSubscript("value") println(value.toString()) %3Cname%3E コンテキスト内の値を JSValue 型で取得できる
JavaScriptCore.framework 実⾏行行⽅方法の詳細
実⾏行行⽅方法の詳細 JavaScript コードを実⾏行行
JavaScript コードを実⾏行行 JavaScript コードの実⾏行行 context.evaluateScript(script) -> JSValue! • 実⾏行行したい JavaScript を⽂文字列列で渡す • 最後に実⾏行行した命令令の参照を受け取れる – 最後が『value;』なら『value の値』 – 最後が『10 + 3;』なら『13』 – 最後が『x = 10;』なら代⼊入後の『x の値』 – 最後が『var x = 10;』だと『undefined』
JavaScript コードを実⾏行行 JavaScript コンテキストから変数を取得 context.objectForKeyedSubscript(name) -> JSValue! • 取得したい変数名を⽂文字列列で渡す • 変数の値を JSValue 型で取得 – toXXXX() メソッドでネイティブ型に変換可能 – 指定した型に合わせて値が変換される – 存在しない名前では undefined が得られる ※ Objective-‐‑‒C なら context[name] で取得可能
JavaScript コードを実⾏行行 JSValue からネイティブ型に変換 • • • • • toInt32() ->Int32 toUint32() ->Uint32 toDouble() ->Double toString() ->String! toBool() ->Bool • • • • toObject() toArray() toDictionary() toNumber() • • • • • toDate() ->NSDate! toPoint() ->CGPoint toSize() ->CGSize toRect() ->CGRect toRange() ->NSRange ->AnyObject! ->[AnyObject]! ->[NSObject:AnyObject]! ->NSNumber!
JavaScript コードを実⾏行行 JSValue の未定義値をネイティブな値に変換 undefined • • • • • toString() toInt32() toDouble() toBool() toObject() ➡ "undefined" ➡ 0 ➡ Double.NaN ➡ false ➡ nil
JavaScript コードを実⾏行行 JSValue の null 値をネイティブな値に変換 null • • • • • toString() toInt32() toDouble() toBool() toObject() ➡ "null" ➡ 0 ➡ 0.0 ➡ false ➡ nil
JavaScript コードを実⾏行行 JSValue の型を判定する • isNumber() • isString() • isBoolean() • isObject() • isNull() • isUndefined()
実⾏行行⽅方法の詳細 JavaScript に直接 変数を登録
JavaScript に直接変数を登録 JavaScript コンテキストに値を登録 context.setObject(value,forKeyedSubscript:name) -> Void • 設定したい変数名を⽂文字列列で渡す (name) – 存在しない名前の場合は新規登録する – 登録済みの名前なら値を上書きする • 設定する値を渡す (value) – 任意のネイティブ型を指定できる – JavaScript はもともと Variant 型 – 内部的には [object number] や [object string] 等で認識識 – nil を渡すと [object undefined] が設定される
JavaScript に直接変数を登録 変数にネイティブオブジェクトも登録可能 詳細は後ほど
実⾏行行⽅方法の詳細 JavaScript に直接 関数を登録
JavaScript に直接関数を登録 Objective-‐‑‒C で関数を登録する場合 context[@"sum"] = ^(NSArray* values) { NSInteger result = 0; for (NSNumber* value in values) { result += value.integerValue; } return result; }; 変数の値として Blocks を渡すだけ で OK
JavaScript に直接関数を登録 Swift で関数を登録する場合 (1/6) context.setObject(value,forKeyedSubscript:name) -> Void • • • • JSContext は Objective-‐‑‒C クラス Objective-‐‑‒C では id 型 で指定する Swift では AnyObject! 型 で指定する Swift クロージャは AnyObject! に渡せない
JavaScript に直接関数を登録 Swift で関数を登録する場合 (2/6) 引数と戻り値が明⽰示的な Blocks 引数には Swift のクロージャを渡せる ➡ JSContext を Objective-‐‑‒C カテゴリ拡張して 明⽰示的な Blocks を受け取るメソッドを作る
JavaScript に直接関数を登録
Swift で関数を登録する場合 (3/6)
JSContext+Closure.h
#import <JavaScriptCore/JavaScriptCore.h>
typedef id (^unaryFunction)(id);
typedef id (^binaryFunction)(id, id);
@interface JSContext (Closure)
- (void)setUnaryFunction:(unaryFunction)function
forKeyedSubscript:(NSString*)key;
- (void)setBinaryFunction:(binaryFunction)function
forKeyedSubscript:(NSString*)key;
@end
JavaScript に直接関数を登録 Swift で関数を登録する場合 (4/6) JSContext+Closure.m - (void)setUnaryFunction:(unaryFunction)function forKeyedSubscript:(NSString*)key { [self setObject:function forKeyedSubscript:key]; } - (void)setBinaryFunction:(binaryFunction)function forKeyedSubscript:(NSString*)key { [self setObject:function forKeyedSubscript:key]; }
JavaScript に直接関数を登録 Swift で関数を登録する場合 (5/6) このヘッダーをブリッジヘッダーにインポートして… $(PROJECT_̲NAME)-‐‑‒Bridging-‐‑‒Header.h #import "JSContext+Closure.h" ブリッジヘッダーは "Swift Compiler -‐‑‒ Code Generation" 設定の “Objective-‐‑‒C Bridging Header” に登録されている
JavaScript に直接関数を登録
Swift で関数を登録する場合 (6/6)
これでクロージャを JavaScript へ登録可能に。
let function = { (values:AnyObject!)->AnyObject in
var sum:Int = 0
for value in values as NSArray
{
sum += value.integerValue
}
return sum
}
context.setUnaryFunction(function,forKeyedSubscript:"sum")
JavaScript に直接関数を登録 登録した関数は JavaScript で普通に利利⽤用可能 context.evaluateScript("sum([10,20,30]);") • 実⾏行行⽅方法は通常の JavaScript のとおり • 結果の取得⽅方法は前述のとおり
実⾏行行⽅方法の詳細 複数⾏行行に渡る JavaScript コードの実⾏行行
複数⾏行行に渡る JavaScript コードの実⾏行行
Case 1:
ひとつの⽂文字列列にまとめて実⾏行行 #1
[OK]
context.evaluateScript(
"var tag='<name>';\n var val=encodeURI(tag);")
• 改⾏行行⽂文字が含まれていても実⾏行行可能
複数⾏行行に渡る JavaScript コードの実⾏行行
Case 2:
ひとつの⽂文字列列にまとめて実⾏行行 #2
[OK]
context.evaluateScript(
"var tag='<name>'; var val=\nencodeURI(tag);")
• JavaScript として適切切であれば
コードの途中に改⾏行行⽂文字を挿⼊入可能
複数⾏行行に渡る JavaScript コードの実⾏行行
Case 3:
各⾏行行を複数回に分けて実⾏行行 #1
context.evaluateScript(
"var tag='<name>';")
context.evaluateScript(
"var val=encodeURI(tag);")
• 実⾏行行結果はコンテキストに蓄積される
• 次の実⾏行行時に値を引き続き利利⽤用可能
[OK]
複数⾏行行に渡る JavaScript コードの実⾏行行
Case 4:
各⾏行行を複数回に分けて実⾏行行 #2
[NG]
context.evaluateScript(
"var tag='<name>'; var val=")
context.evaluateScript(
"encodeURI(tag);")
• ⾏行行の途中での evaluateScript はできない
• SyntaxError: Unexpected EOF 例例外エラー
複数⾏行行に渡る JavaScript コードの実⾏行行 Case 5: 各⾏行行を複数回に分けて実⾏行行 #3 [NG] context.evaluateScript("if (value==1)") context.evaluateScript("{ (text="Yes") }") context.evaluateScript("else") context.evaluateScript("{ (text="No") }") • 各⾏行行が独⽴立立して実⾏行行される • 条件分岐が正しく⾏行行われない • ひとつの evaluateScript で実⾏行行すれば OK
複数⾏行行に渡る JavaScript コードの実⾏行行 Case 6: 各⾏行行を複数回に分けて実⾏行行 #4 [NG] context.evaluateScript("try {") context.evaluateScript("value=XXXX;") context.evaluateScript("} catch (e)") context.evaluateScript(“{ value=0; }") • 各⾏行行が独⽴立立して実⾏行行される • 例例外が正しくハンドルされない • ひとつの evaluateScript で実⾏行行すれば OK
複数⾏行行に渡る JavaScript コードの実⾏行行 複数⾏行行の JavaScript は 意味的に不不⾜足のない単位で 実⾏行行すること
実⾏行行⽅方法の詳細 JavaScript の 実⾏行行時エラーを検出
JavaScript の実⾏行行時エラーを検出 evaluateScript でエラーが発⽣生すると… JavaScript 内で 例例外エラーが発⽣生する
JavaScript の実⾏行行時エラーを検出 ⼀一般的な JavaScript 例例外オブジェクト • • • • Error SyntaxError TypeError EvalError • RangeError • ReferenceError • URIError
JavaScript の実⾏行行時エラーを検出 JavaScript 内で発⽣生した例例外は ネイティブコードで検出可能
JavaScript の実⾏行行時エラーを検出 JavaScript 例例外をネイティブコードで検出する context.exceptionHandler :((JSContext!,JSValue!)->Void)! • コンテキストに exceptionHandler を登録 • 例例外が発⽣生すると関数が呼び出される
JavaScript 例例外をネイティブコードで検出する
exceptionHandler を登録
context.exceptionHandler = {
(context:JSContext!, exception:JSValue!)->Void in
println("Error: \(exception.toString())")
};
• context:
• exception:
実⾏行行した JSContext を取得
例例外オブジェクトを取得
JavaScript 例例外をネイティブコードで検出する exception から詳細情報を取得 • .toString() – エラーメッセージ を取得 – "SyntaxError: Expected token ':'" など • .toDictionary()["line"] as? NSNumber – エラーが発⽣生した ⾏行行番号 を取得 – evaluateScript に渡した⽂文字列列内での⾏行行番号 • .toDictionary()["stack"] as? NSString – 関数スタック を取得する – 改⾏行行⽂文字で区切切って関数名が記録される – 構⽂文エラーなど、スタック情報がない場合は nil
JavaScript の実⾏行行時エラーを検出 exception オブジェクトは 例例外 Error オブジェクトそのもの
JavaScript の実⾏行行時エラーを検出 JavaScript からカスタムエラーを送出可能 context.evaluateScript("throw Error(message);") • JavaScript から Error 例例外を送出 • exceptionHandler で受け取れる • エラーメッセージは "Error: message"
JavaScript の実⾏行行時エラーを検出 カスタムエラーの名称を指定可能 context.evaluateScript( "var error=Error();" + "error.name='MyError';" + "error.message='message';" + "throw error;") • 独⾃自名の例例外を送出 • エラーメッセージは "MyError: message"
JavaScriptCore.framework ネイティブオブジェクトの利利⽤用
ネイティブオブジェクトの利利⽤用 利利⽤用⽅方法 1. JavaScript で使える機能を宣⾔言 2. ネイティブオブジェクトを⽣生成 3. JavaScript からプロパティを参照 4. JavaScript からメソッドを実⾏行行
ネイティブオブジェクトの利利⽤用 JavaScript で 使える機能を宣⾔言
JavaScript で使える機能を宣⾔言 JSExport を継承したプロトコルを作成 import JavaScriptCore @objc protocol EZObjectJSExport: JSExport { var name:String { get set } var value:String { get set } func set(name:String, _ value:String)->Void func toData()->NSData } @objc 指定⼦子を忘れないこと
JavaScript で使える機能を宣⾔言 先ほどのプロトコルを継承したクラスを実装 public class EZObject: NSObject, EZObjectJSExport { public override init() { … } public var name:String { … } public var value:String { … } public func set(name:String, _ value:String)->Void { … } public func toData()->NSData { … } } 必ず NSObject を継承すること
JavaScript で使える機能を宣⾔言 オブジェクトの定義完了了 JSExport を継承したプロトコル内で定義した 機能だけを JavaScript から 直接 利利⽤用できる • 未定義のメソッドを呼び出すと "TypeError: 'undefined' is not a function" • 未定義のプロパティを呼び出すと "undefined" この辺りの挙動は JavaScript で「存在しないもの」を扱うのと同じ
ネイティブオブジェクトの利利⽤用 ネイティブオブジェクトの インスタンスを作る 1. ネイティブコードから⽣生成する⽅方法 2. JavaScript 内で⽣生成する⽅方法
ネイティブオブジェクトのインスタンスを作る ネイティブコードから⽣生成する⽅方法 let object = EZObject() context.setObject(object, forKeyedSubscript:"obj") • ネイティブコードで⽣生成したインスタンスを コンテキストの 変数にそのまま登録 • JavaScript 内から変数をとおして利利⽤用可能
ネイティブオブジェクトのインスタンスを作る
JavaScript 内で⽣生成する⽅方法(追加準備)
インスタンスを⽣生成するクラスメソッドを追加
@objc protocol EZObjectJSExport: JSExport
{
class func create()->AnyObject
}
public class EZObject: NSObject, EZObjectJSExport
{
public class func create() -> AnyObject
{
return EZObject();
}
}
ネイティブオブジェクトのインスタンスを作る JavaScript 内で⽣生成する⽅方法(実装) context.setObject(EZObject.self, forKeyedSubscript:"EZObject") context.evaluateScript("var obj=EZObject.create();") • クラス情報をコンテキストに登録 • クラスメソッドを使ってインスタンス⽣生成
ネイティブオブジェクトのインスタンスを作る JavaScript 内で⽣生成する⽅方法(余談) let object = context.objectForKeyedSubscript("obj") .toObject() as EZObject • ネイティブコードへの取り出しも可能
ネイティブオブジェクトの利利⽤用 JavaScript から プロパティを使⽤用
JavaScript からプロパティを使⽤用 ネイティブコードのプロパティを読み書き context.evaluateScript("var name = obj.name;") context.evaluateScript("obj.value = 'NewValue';") • プロパティ名の後には括弧不不要 • JavaScript どおりの⽅方法で読み書き可能 括弧をつけると "Type Error: 'PROP' is not a function" エラー
ネイティブオブジェクトの利利⽤用 JavaScript から メソッドを実⾏行行
JavaScript からメソッドを実⾏行行 ネイティブコードのメソッドを実⾏行行 context.evaluateScript("obj.set('NewName','NewValue');") context.evaluateScript("var data = obj.toData();") • JavaScript どおりの⽅方法で実⾏行行可能 • 引数を取らないメソッドも括弧が必要 括弧をつけないとメソッドそのものが得られる
ネイティブオブジェクトの利利⽤用 メソッド実装時の注意
JavaScript からメソッドを実⾏行行 メソッド実装時の注意 #1 toString() メソッドは実装しない • JavaScript 組み込みの toString() が優先 • 独⾃自に実装しても呼び出されない (Beta 5)
JavaScript からメソッドを実⾏行行 メソッド実装時の注意 #2 Swift のメソッドは 引数のラベルが反映された名称になる func set(name:String, value:String)->Void ➡ void setValue(name, value) func set(#name:String, value:String)->Void ➡ void setWithNameValue(name, value) func set(name:String, _ value:String)->Void ➡ void set(name, value)
JavaScriptCore.framework 相互運⽤用 JavaScript とネイティブコード
JavaScript とネイティブコードの相互運⽤用 JavaScript 関数を ネイティブコードで実⾏行行
JavaScript 関数をネイティブコードで実⾏行行 JavaScript 関数の取得と実⾏行行 context.evaluateScript("function sum(array) { … }") let sum = context.objectForKeyedSubscript("sum") let result = sum.callWithArguments([ [1,3,5,7] ]) • 取得時は引数を添えずに関数名を指定する – JavaScript 関数の参照 を取得可能 • 実⾏行行時は引数を配列列で渡す – 複数の引数を渡せる – 今回は配列列を取る関数なので配列列を配列列に⼊入れている
JavaScript とネイティブコードの相互運⽤用 JavaScript オブジェクトと ネイティブオブジェクトの相互運⽤用
オブジェクトの相互運⽤用 おさらい ネイティブオブジェクトを JavaScript に取り込む場合 let object = EZObject() context.setObject(object, forKeyedSubscript:"obj") JavaScript からネイティブオブジェクトを取得する場合 context.setObject(EZObject.self, forKeyedSubscript:"EZObject") context.evaluateScript("var obj=EZObject.create();") let object = context.objectForKeyedSubscript("obj")
オブジェクトの相互運⽤用 どちらとも 相互にオブジェクトを操作可能
オブジェクトの相互運⽤用 JavaScript での変更更がネイティブコードに反映 context.evaluateScript("obj.value = 'FromJS';") println(object.value) • JavaScript で設定した値を 直ぐに ネイティブオブジェクトから利利⽤用可能
オブジェクトの相互運⽤用 ネイティブコードでの変更更が JavaScript に反映 object.value = "FromNative" context.evaluateScript("var value=obj.value;") let value = context.objectForKeyedSubscript("value") println(value) • ネイティブオブジェクトで設定した値を 直ぐに JavaScript から利利⽤用可能
おまけ JavaScriptCore.framework 便便利利な使い⽅方
便便利利な使い⽅方 スクリプトを ファイルから読み込んで実⾏行行
スクリプトをファイルから読み込んで実⾏行行 バンドルからファイルを読み込む var bundle = NSBundle.mainBundle() var path = bundle.pathForResource("Script",ofType:"js") let script = NSString(contentsOfFile:path, encoding:NSUTF8StringEncoding, error: nil) context.evaluateScript(script) let result = context.objectForKeyedSubscript("answer") スクリプトをリソースとして管理理できる
スクリプトをファイルから読み込んで実⾏行行 読み込む JavaScript ファイル function sum(array) { var result = 0; for (var i = 0; i < array.length; ++i) { result += array[i]; } return result; } var answer = sum([1,10,100,1000]); 素のテキストとして扱えるので編集が簡単
便便利利な使い⽅方 return 命令令で 終われるスクリプトにする
return 命令令で終われるスクリプトにする 読み込む JavaScript ファイル function sum(array) { var result = 0; for (var i = 0; i < array.length; ++i) { result += array[i]; } return result; } return sum([1,10,100,1000]); 最後を return 命令令で終わらせたい
オブジェクトの相互運⽤用 そのまま使うと… SyntaxError: Return statements are only valid inside functions return 命令令は関数内で使わなければいけない
return 命令令で終われるスクリプトにする スクリプトを実⾏行行時に匿匿名関数で包む let result = context.evaluateScript("(function(){\(script)})();") • スクリプトを関数内に⼊入れて関数を実⾏行行 • return の値は実⾏行行結果として取得可能 • 上記のとおり1⾏行行で記載すれば、 エラー時に通知される⾏行行番号が狂わない
JavaScriptCore.framework • • • • • • JavaScript は⼿手軽に使えるスクリプト⾔言語 JavaScript コードをアプリ内で簡単に実⾏行行 ネイティブオブジェクトとの相互運⽤用が可能 JavaScript ライブラリをネイティブコードで活⽤用 コンパイル不不要でスクリプトを差し替え可能 カスタムスクリプト機能を実装するのに便便利利 可能性を秘めたフレームワーク