JavaScriptCore.framework の普通な使い方 #cocoa_kansai

>100 Views

August 09, 14

スライド概要

Objective-C や Swift のネイティブコードから JavaScript をランタイムで実行するための JavaScriptCore.framework のお話です。基本的な機能の説明と、注意点を整理して紹介しています。

※ Docswell での公開に移行する直前の Slideshare での閲覧数は 38,607 でした。

profile-image

正統派趣味人プログラマー。プログラミングとは幼馴染です。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

JavaScriptCore framework の普通な使い⽅方 EZ-‐‑‒NET  熊⾕谷友宏    @es_̲kumagai http://program.station.ez-‐‑‒net.jp/

2.

⾃自⼰己紹介 EZ-‐‑‒NET  熊⾕谷友宏   http://program.station.ez-‐‑‒net.jp/ @es_̲kumagai EZ-‐‑‒NET  IP  Phone ⾳音で再配達ゴッド ⾳音で再配達 ⾳音でダイヤル いつもの電卓 for  iPad いつもの電卓 for  iPhone

3.

書籍 • • • • • • • • • Xcode  全機能を網羅羅 プロジェクトの作り⽅方 ソースコード編集の効率率率化 ショートカットキーの紹介 オートレイアウトの使い⽅方 ローカライズの設定⽅方法 バージョン管理理の使い⽅方 ビルド設定とスキーム設定 ほか、とにかくいろいろ こんなご時世ですが ぜひ⼿手に取ってパラパラめくってみてください。 特設サイト  ̶—  http://ez-‐‑‒net.jp/sp/xcode5/

4.

JavaScriptCore.framework

5.

特徴 JavaScript  ⾔言語 1. Web  でお馴染みのスクリプト⾔言語 2. ⼿手軽にコードを組み⽴立立てられる 3. JavaScript  を使える⼈人は多いはず

6.

特徴 JavaScriptCore.framework 1. アプリ内で  JavaScript  を実⾏行行可能 2. ネイティブコードとの相互運⽤用が可能 OS  X  10.9、iOS  7.0  から利利⽤用可能

7.

特徴 1.  アプリ内で  JavaScript  を実⾏行行可能 1. スクリプトをテキストで⽤用意する 2. 実⾏行行する直前までに⽤用意すれば良良い 3. ビルドに依らない実装が可能になる     ➡  コードを⾃自由に差し替えられる     ➡  リソースと同じように  DL  適⽤用できる     ➡  アプリ使⽤用者にカスタムスクリプトを         書かせる機能を容易易に実現できる

8.

特徴 2.  ネイティブコードとの相互運⽤用 1. 変数の値を⾃自由に受け渡しできる ➡  ネイティブの⾃自作クラスも交換可能 2. JavaScript  からネイティブコードの メソッドを実⾏行行できる 3. ネイティブコードから JavaScript  の関数を実⾏行行できる

9.

JavaScriptCore.framework 実⾏行行の流流れ

10.

実⾏行行⼿手順  (1/4) JavaScriptCore  をインポート import JavaScriptCore ターゲット設定の  Linked  Frameworks  and  Libraries  で JavaScriptCore.framework  をリンクしておくこと

11.

実⾏行行⼿手順  (2/4) 実⾏行行環境のコンテキストを⽣生成   let context = JSContext() このコンテキスト内で  JavaScript  を実⾏行行する

12.

実⾏行行⼿手順  (3/4) JavaScript  コードを実⾏行行 let script = "var value = encodeURI('<name>');" context.evaluateScript(script) 実⾏行行結果はコンテキスト内に蓄積される

13.

実⾏行行⼿手順  (4/4) コンテキストから値を取得 let value:JSValue = context.objectForKeyedSubscript("value") println(value.toString()) %3Cname%3E コンテキスト内の値を  JSValue  型で取得できる

14.

JavaScriptCore.framework 実⾏行行⽅方法の詳細

15.

実⾏行行⽅方法の詳細 JavaScript  コードを実⾏行行

16.

JavaScript  コードを実⾏行行 JavaScript  コードの実⾏行行 context.evaluateScript(script) -> JSValue! • 実⾏行行したい  JavaScript  を⽂文字列列で渡す • 最後に実⾏行行した命令令の参照を受け取れる – 最後が『value;』なら『value  の値』 – 最後が『10  +  3;』なら『13』 – 最後が『x  =  10;』なら代⼊入後の『x  の値』 – 最後が『var  x  =  10;』だと『undefined』

17.

JavaScript  コードを実⾏行行 JavaScript  コンテキストから変数を取得 context.objectForKeyedSubscript(name) -> JSValue! • 取得したい変数名を⽂文字列列で渡す • 変数の値を  JSValue  型で取得 – toXXXX()  メソッドでネイティブ型に変換可能 – 指定した型に合わせて値が変換される – 存在しない名前では  undefined  が得られる ※  Objective-‐‑‒C  なら  context[name]  で取得可能

18.

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!

19.

JavaScript  コードを実⾏行行 JSValue  の未定義値をネイティブな値に変換 undefined • • • • • toString() toInt32() toDouble() toBool() toObject() ➡ "undefined" ➡ 0 ➡ Double.NaN ➡ false ➡ nil

20.

JavaScript  コードを実⾏行行 JSValue  の  null  値をネイティブな値に変換 null • • • • • toString() toInt32() toDouble() toBool() toObject() ➡ "null" ➡ 0 ➡ 0.0 ➡ false ➡ nil

21.

JavaScript  コードを実⾏行行 JSValue  の型を判定する • isNumber() • isString() • isBoolean() • isObject() • isNull() • isUndefined()

22.

実⾏行行⽅方法の詳細 JavaScript  に直接 変数を登録

23.

JavaScript  に直接変数を登録 JavaScript  コンテキストに値を登録 context.setObject(value,forKeyedSubscript:name) -> Void • 設定したい変数名を⽂文字列列で渡す  (name) – 存在しない名前の場合は新規登録する – 登録済みの名前なら値を上書きする • 設定する値を渡す  (value) – 任意のネイティブ型を指定できる – JavaScript  はもともと  Variant  型 – 内部的には  [object  number]  や  [object  string]  等で認識識 – nil  を渡すと  [object  undefined]  が設定される

24.

JavaScript  に直接変数を登録 変数にネイティブオブジェクトも登録可能 詳細は後ほど

25.

実⾏行行⽅方法の詳細 JavaScript  に直接 関数を登録

26.

JavaScript  に直接関数を登録 Objective-‐‑‒C  で関数を登録する場合 context[@"sum"] = ^(NSArray* values) { NSInteger result = 0; for (NSNumber* value in values) { result += value.integerValue; } return result; }; 変数の値として  Blocks  を渡すだけ  で  OK

27.

JavaScript  に直接関数を登録 Swift  で関数を登録する場合  (1/6) context.setObject(value,forKeyedSubscript:name) -> Void • • • • JSContext  は  Objective-‐‑‒C  クラス Objective-‐‑‒C  では  id  型  で指定する Swift  では  AnyObject!  型  で指定する Swift  クロージャは  AnyObject!  に渡せない

28.

JavaScript  に直接関数を登録 Swift  で関数を登録する場合  (2/6) 引数と戻り値が明⽰示的な  Blocks  引数には Swift  のクロージャを渡せる ➡  JSContext  を  Objective-‐‑‒C  カテゴリ拡張して     明⽰示的な  Blocks  を受け取るメソッドを作る

29.
[beta]
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

30.

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]; }

31.

JavaScript  に直接関数を登録 Swift  で関数を登録する場合  (5/6) このヘッダーをブリッジヘッダーにインポートして… $(PROJECT_̲NAME)-‐‑‒Bridging-‐‑‒Header.h #import "JSContext+Closure.h" ブリッジヘッダーは  "Swift  Compiler  -‐‑‒  Code  Generation"  設定の “Objective-‐‑‒C  Bridging  Header”  に登録されている

32.
[beta]
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")

33.

JavaScript  に直接関数を登録 登録した関数は  JavaScript  で普通に利利⽤用可能 context.evaluateScript("sum([10,20,30]);") • 実⾏行行⽅方法は通常の  JavaScript  のとおり • 結果の取得⽅方法は前述のとおり

34.

実⾏行行⽅方法の詳細 複数⾏行行に渡る JavaScript  コードの実⾏行行

35.
[beta]
複数⾏行行に渡る  JavaScript  コードの実⾏行行
Case  1:
ひとつの⽂文字列列にまとめて実⾏行行  #1

[OK]

context.evaluateScript(
"var tag='<name>';\n var val=encodeURI(tag);")

• 改⾏行行⽂文字が含まれていても実⾏行行可能

36.
[beta]
複数⾏行行に渡る  JavaScript  コードの実⾏行行
Case  2:
ひとつの⽂文字列列にまとめて実⾏行行  #2

[OK]

context.evaluateScript(
"var tag='<name>'; var val=\nencodeURI(tag);")

• JavaScript  として適切切であれば
コードの途中に改⾏行行⽂文字を挿⼊入可能

37.
[beta]
複数⾏行行に渡る  JavaScript  コードの実⾏行行
Case  3:
各⾏行行を複数回に分けて実⾏行行  #1

context.evaluateScript(
"var tag='<name>';")
context.evaluateScript(
"var val=encodeURI(tag);")
• 実⾏行行結果はコンテキストに蓄積される
• 次の実⾏行行時に値を引き続き利利⽤用可能

[OK]

38.
[beta]
複数⾏行行に渡る  JavaScript  コードの実⾏行行
Case  4:
各⾏行行を複数回に分けて実⾏行行  #2

[NG]

context.evaluateScript(
"var tag='<name>'; var val=")
context.evaluateScript(
"encodeURI(tag);")
• ⾏行行の途中での  evaluateScript  はできない
• SyntaxError:  Unexpected  EOF  例例外エラー

39.

複数⾏行行に渡る  JavaScript  コードの実⾏行行 Case  5: 各⾏行行を複数回に分けて実⾏行行  #3 [NG] context.evaluateScript("if (value==1)") context.evaluateScript("{ (text="Yes") }") context.evaluateScript("else") context.evaluateScript("{ (text="No") }") • 各⾏行行が独⽴立立して実⾏行行される • 条件分岐が正しく⾏行行われない • ひとつの  evaluateScript  で実⾏行行すれば  OK

40.

複数⾏行行に渡る  JavaScript  コードの実⾏行行 Case  6: 各⾏行行を複数回に分けて実⾏行行  #4 [NG] context.evaluateScript("try {") context.evaluateScript("value=XXXX;") context.evaluateScript("} catch (e)") context.evaluateScript(“{ value=0; }") • 各⾏行行が独⽴立立して実⾏行行される • 例例外が正しくハンドルされない • ひとつの  evaluateScript  で実⾏行行すれば  OK

41.

複数⾏行行に渡る  JavaScript  コードの実⾏行行 複数⾏行行の  JavaScript  は 意味的に不不⾜足のない単位で 実⾏行行すること

42.

実⾏行行⽅方法の詳細 JavaScript  の 実⾏行行時エラーを検出

43.

JavaScript  の実⾏行行時エラーを検出 evaluateScript  でエラーが発⽣生すると… JavaScript  内で 例例外エラーが発⽣生する

44.

JavaScript  の実⾏行行時エラーを検出 ⼀一般的な  JavaScript  例例外オブジェクト • • • • Error SyntaxError TypeError EvalError • RangeError • ReferenceError • URIError

45.

JavaScript  の実⾏行行時エラーを検出 JavaScript  内で発⽣生した例例外は ネイティブコードで検出可能

46.

JavaScript  の実⾏行行時エラーを検出 JavaScript  例例外をネイティブコードで検出する context.exceptionHandler :((JSContext!,JSValue!)->Void)! • コンテキストに  exceptionHandler  を登録 • 例例外が発⽣生すると関数が呼び出される

47.
[beta]
JavaScript  例例外をネイティブコードで検出する
exceptionHandler  を登録
context.exceptionHandler = {
(context:JSContext!, exception:JSValue!)->Void in
println("Error: \(exception.toString())")
};

• context:  
• exception:

実⾏行行した  JSContext  を取得
例例外オブジェクトを取得

48.

JavaScript  例例外をネイティブコードで検出する exception  から詳細情報を取得 • .toString() – エラーメッセージ  を取得 – "SyntaxError:  Expected  token  ':'"  など • .toDictionary()["line"] as? NSNumber – エラーが発⽣生した  ⾏行行番号  を取得 – evaluateScript  に渡した⽂文字列列内での⾏行行番号 • .toDictionary()["stack"] as? NSString – 関数スタック  を取得する – 改⾏行行⽂文字で区切切って関数名が記録される – 構⽂文エラーなど、スタック情報がない場合は  nil

49.

JavaScript  の実⾏行行時エラーを検出 exception  オブジェクトは 例例外  Error  オブジェクトそのもの

50.

JavaScript  の実⾏行行時エラーを検出 JavaScript  からカスタムエラーを送出可能 context.evaluateScript("throw Error(message);") • JavaScript  から  Error  例例外を送出 • exceptionHandler  で受け取れる • エラーメッセージは  "Error:  message"

51.

JavaScript  の実⾏行行時エラーを検出 カスタムエラーの名称を指定可能 context.evaluateScript( "var error=Error();" + "error.name='MyError';" + "error.message='message';" + "throw error;") • 独⾃自名の例例外を送出 • エラーメッセージは  "MyError:  message"

52.

JavaScriptCore.framework ネイティブオブジェクトの利利⽤用

53.

ネイティブオブジェクトの利利⽤用 利利⽤用⽅方法 1. JavaScript  で使える機能を宣⾔言 2. ネイティブオブジェクトを⽣生成 3. JavaScript  からプロパティを参照 4. JavaScript  からメソッドを実⾏行行

54.

ネイティブオブジェクトの利利⽤用 JavaScript  で 使える機能を宣⾔言

55.

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  指定⼦子を忘れないこと

56.

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  を継承すること

57.

JavaScript  で使える機能を宣⾔言 オブジェクトの定義完了了 JSExport  を継承したプロトコル内で定義した 機能だけを  JavaScript  から  直接  利利⽤用できる • 未定義のメソッドを呼び出すと "TypeError:  'undefined'  is  not  a  function" • 未定義のプロパティを呼び出すと  "undefined" この辺りの挙動は  JavaScript  で「存在しないもの」を扱うのと同じ

58.

ネイティブオブジェクトの利利⽤用 ネイティブオブジェクトの インスタンスを作る 1. ネイティブコードから⽣生成する⽅方法 2. JavaScript  内で⽣生成する⽅方法

59.

ネイティブオブジェクトのインスタンスを作る ネイティブコードから⽣生成する⽅方法 let object = EZObject() context.setObject(object, forKeyedSubscript:"obj") • ネイティブコードで⽣生成したインスタンスを コンテキストの  変数にそのまま登録 • JavaScript  内から変数をとおして利利⽤用可能

60.
[beta]
ネイティブオブジェクトのインスタンスを作る
JavaScript  内で⽣生成する⽅方法(追加準備)
インスタンスを⽣生成するクラスメソッドを追加
@objc protocol EZObjectJSExport: JSExport
{
class func create()->AnyObject
}
public class EZObject: NSObject, EZObjectJSExport
{
public class func create() -> AnyObject
{
return EZObject();
}
}

61.

ネイティブオブジェクトのインスタンスを作る JavaScript  内で⽣生成する⽅方法(実装) context.setObject(EZObject.self, forKeyedSubscript:"EZObject") context.evaluateScript("var obj=EZObject.create();") • クラス情報をコンテキストに登録 • クラスメソッドを使ってインスタンス⽣生成

62.

ネイティブオブジェクトのインスタンスを作る JavaScript  内で⽣生成する⽅方法(余談) let object = context.objectForKeyedSubscript("obj") .toObject() as EZObject • ネイティブコードへの取り出しも可能

63.

ネイティブオブジェクトの利利⽤用 JavaScript  から プロパティを使⽤用

64.

JavaScript  からプロパティを使⽤用 ネイティブコードのプロパティを読み書き context.evaluateScript("var name = obj.name;") context.evaluateScript("obj.value = 'NewValue';") • プロパティ名の後には括弧不不要 • JavaScript  どおりの⽅方法で読み書き可能 括弧をつけると  "Type  Error:  'PROP'  is  not  a  function"  エラー

65.

ネイティブオブジェクトの利利⽤用 JavaScript  から メソッドを実⾏行行

66.

JavaScript  からメソッドを実⾏行行 ネイティブコードのメソッドを実⾏行行 context.evaluateScript("obj.set('NewName','NewValue');") context.evaluateScript("var data = obj.toData();") • JavaScript  どおりの⽅方法で実⾏行行可能 • 引数を取らないメソッドも括弧が必要 括弧をつけないとメソッドそのものが得られる

67.

ネイティブオブジェクトの利利⽤用 メソッド実装時の注意

68.

JavaScript  からメソッドを実⾏行行 メソッド実装時の注意  #1 toString()  メソッドは実装しない • JavaScript  組み込みの  toString()  が優先 • 独⾃自に実装しても呼び出されない  (Beta  5)

69.

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)

70.

JavaScriptCore.framework 相互運⽤用 JavaScript  とネイティブコード

71.

JavaScript  とネイティブコードの相互運⽤用 JavaScript  関数を ネイティブコードで実⾏行行

72.

JavaScript  関数をネイティブコードで実⾏行行 JavaScript  関数の取得と実⾏行行 context.evaluateScript("function sum(array) { … }") let sum = context.objectForKeyedSubscript("sum") let result = sum.callWithArguments([ [1,3,5,7] ]) • 取得時は引数を添えずに関数名を指定する – JavaScript  関数の参照  を取得可能 • 実⾏行行時は引数を配列列で渡す – 複数の引数を渡せる – 今回は配列列を取る関数なので配列列を配列列に⼊入れている

73.

JavaScript  とネイティブコードの相互運⽤用 JavaScript  オブジェクトと ネイティブオブジェクトの相互運⽤用

74.

オブジェクトの相互運⽤用 おさらい ネイティブオブジェクトを  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")

75.

オブジェクトの相互運⽤用 どちらとも 相互にオブジェクトを操作可能

76.

オブジェクトの相互運⽤用 JavaScript  での変更更がネイティブコードに反映 context.evaluateScript("obj.value = 'FromJS';") println(object.value) • JavaScript  で設定した値を 直ぐに  ネイティブオブジェクトから利利⽤用可能

77.

オブジェクトの相互運⽤用 ネイティブコードでの変更更が  JavaScript  に反映 object.value = "FromNative" context.evaluateScript("var value=obj.value;") let value = context.objectForKeyedSubscript("value") println(value) • ネイティブオブジェクトで設定した値を 直ぐに  JavaScript  から利利⽤用可能

78.

おまけ JavaScriptCore.framework 便便利利な使い⽅方

79.

便便利利な使い⽅方 スクリプトを ファイルから読み込んで実⾏行行

80.

スクリプトをファイルから読み込んで実⾏行行 バンドルからファイルを読み込む 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") スクリプトをリソースとして管理理できる

81.

スクリプトをファイルから読み込んで実⾏行行 読み込む  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]); 素のテキストとして扱えるので編集が簡単

82.

便便利利な使い⽅方 return  命令令で 終われるスクリプトにする

83.

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  命令令で終わらせたい

84.

オブジェクトの相互運⽤用 そのまま使うと… SyntaxError: Return  statements  are only  valid  inside  functions return  命令令は関数内で使わなければいけない

85.

return  命令令で終われるスクリプトにする スクリプトを実⾏行行時に匿匿名関数で包む let result = context.evaluateScript("(function(){\(script)})();") • スクリプトを関数内に⼊入れて関数を実⾏行行 • return  の値は実⾏行行結果として取得可能 • 上記のとおり1⾏行行で記載すれば、 エラー時に通知される⾏行行番号が狂わない

86.

JavaScriptCore.framework • • • • • • JavaScript  は⼿手軽に使えるスクリプト⾔言語 JavaScript  コードをアプリ内で簡単に実⾏行行 ネイティブオブジェクトとの相互運⽤用が可能 JavaScript  ライブラリをネイティブコードで活⽤用 コンパイル不不要でスクリプトを差し替え可能 カスタムスクリプト機能を実装するのに便便利利 可能性を秘めたフレームワーク