OSX アプリケーション内に WebView を埋め込んだとき、その中で動作している JavaScript 環境とは WebScriptObject を介してやりとりを行う。
JavaScript の関数を呼び出すには evaluateScript: か callWebScriptMethod:withArguments: を利用する。
後者の場合、引数の配列には NSNumber, NSString, NSArray 等を渡すことができるが、NSDictionary を渡すことはできない。
ちなみにこの仕様についてはリファレンスには記載されていないが、WebScriptObject.h を見ると確認することができる。
連想配列を渡すには、WebScriptObject にすればいいようなので、次のように実装してみた。
WebScriptObject+Convenience.h
#import <WebKit/WebKit.h>
@interface WebScriptObject (Convenience)
// TODO: Method Swizzling で callWebScriptMethod: をオーバライドしたい
- (id)callMethod:(NSString*)name args:(NSArray*)args;
@end
WebScriptObject+Convenience.m
#import "WebScriptObject+Convenience.h"
#define TEMP_VARNAME @"__wso_temp"
@implementation WebScriptObject (Convenience)
- (id)webScriptObjectFromDictionary:(NSDictionary*)dict
{
// NOTE: 連想配列に対する WebScriptObject の生成は evaluateWebScript: でやるしかなく,
// 適当な変数に入れておかないと参照数が 0 になってしまう
// 生成した連想配列は TEMP_VARNAME に一時保管する
NSError* error = nil;
NSData* json = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&error];
NSString* script = [NSString stringWithFormat:
TEMP_VARNAME @".push(%@);"
TEMP_VARNAME @"[" TEMP_VARNAME ".length - 1];",
[[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]];
//NSLog(@"script=%@", script);
return [self evaluateWebScript:script];
}
- (id)callMethod:(NSString*)name args:(NSArray*)args_
{
[self evaluateWebScript:TEMP_VARNAME @" = [];"];
// WebScriptObject は NSDictionary を連想配列に変換してくれないため, 自力で行う
NSMutableArray* args = [NSMutableArray array];
for (id arg in args_) {
if ([arg isKindOfClass:[NSDictionary class]]) {
[args addObject:[self webScriptObjectFromDictionary:arg]];
} else {
[args addObject:arg];
}
}
id result = [self callWebScriptMethod:name withArguments:args];
[self removeWebScriptKey:TEMP_VARNAME];
return result;
}
@end
ポイントは以下のとおり。
- WebScriptObject の生成は evaluateScript: を使う。
- evaluateScript: に渡せるよう、NSJSONSerialization で NSDictionary を JSON にする。
- evaluateScript: の戻り値の参照数が 0 にならないよう、一時変数に参照を持たせておく。
JSON にしている時点で効率が悪いので、callWebScriptMethod: は使わずいっそ全て evaluateScript: でやった方がシンプルかもね...
OSX 10.9 からは JavaScriptCore.framework が使いやすくなるようなので、こんなまわりくどいことはやらなくて良くなるかもしれない。