読者です 読者をやめる 読者になる 読者になる

PhoneGap/CordovaがiOSのネイティブ機能を使うしくみ

iOS

さいきんWebとiOS/Andoroid Native Appの融合がすすんでいますね!

ブラウザコンポーネント(WebViewという)のJSからiOSのネイティブ機能を呼び出すスタイルも流行っています. PhoneGapとかFacebookアプリとかね!


自分は勝手にJS-Native Bridgeと呼んでいるのですが,
あれってどうやってるのって話


Google先生で「uiwebview native」でぐぐる
UIWebViewでWebとネイティブを相互連携させる方法について
iPhoneのネイティブ機能をWebViewから呼び出す方法

こういう記事が出てきます
ようするに

Obj-C → JS [webView stringByEvaluatingJavaScriptFromString:@"JSCode"]
JS → Obj-C webView:shouldStartLoadWithRequest:navigationType: を発火させる

だが甘い

↑の記事ではあくまで原理の解説にしかなっていません.
現実のコードで使うにはもっと改良が必要なのです.


具体的には:

  • iframe fire
  • big size data support
  • queuing

iframe fire

で,イベントを発火させる方法として↑ではaタグをクリックさせたり,location.hrefをいじっていますが,そんなことしなくてもiframeを作ってやると発火します.こっちのほうが影響範囲が少なくクリーンです.


https://github.com/apache/incubator-cordova-js/blob/master/lib/ios/exec.js

gapBridge = document.createElement("iframe");
gapBridge.setAttribute("style", "display:none;");
gapBridge.setAttribute("height","0px");
gapBridge.setAttribute("width","0px");
gapBridge.setAttribute("frameborder","0");
document.documentElement.appendChild(gapBridge);
gapBridge.src = "gap://ready";

これでイベントが発生してくれます

big size data support

URLでもある程度の情報は伝えられるのですが,あまり大きなものはダメです.
特に画像とか.そのためデータはURIに付加するのではなくJSON化してstringByEvaluatingJavaScriptFromString経由で取得するようにします.
↓の擬似コードみたいなイメージ

js

cordova.command = JSON.stringify(commandArgs);
createIframe();

Obj-C

- (BOOL) webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
  NSURL* url = [request URL];
  if ([[url scheme] isEqualToString:@"gap"]) {
    NSString *json = [webView stringByEvaluatingJavaScriptFromString:@"cordova.command"];
    NSDictionary *params = [NSJSONSerialization dataWithJSONObject:json options:nil error:nil]
    [self dosomething] // なんかする
    return NO;
  }
}

queuing

webView:shouldStartLoadWithRequest:はある一定時間内に複数回発生させようとしても1回しか発生しません.おそらくJSのrun loopに戻れるまでイベントが発生させられないことなどが関係しているような.

cordova.command = JSON.stringify(commandArgs) // 無視される
createIframe();
cordova.command = JSON.stringify(commandArgs2)
createIframe();

そのためcommandのqueuingが必要となります.

cordovaではcommandをcommandQueueという配列に入れ,commandQueueFlushingというフラグで管理することで対処しています.


https://github.com/apache/incubator-cordova-js/blob/master/lib/ios/exec.js

if (cordova.commandQueue.length == 1 && !cordova.commandQueueFlushing) {
    if (!gapBridge) {
        createGapBridge();
    }
    gapBridge.src = "gap://ready";
}

https://github.com/apache/incubator-cordova-ios/blob/master/CordovaLib/Classes/CDVViewController.m

- (void) flushCommandQueue
{
    [self.webView stringByEvaluatingJavaScriptFromString:@"cordova.commandQueueFlushing = true"];

    // Keep executing the command queue until no commands get executed.
    // This ensures that commands that are queued while executing other
    // commands are executed as well.
    int numExecutedCommands = 0;
    do {
        numExecutedCommands = [self executeQueuedCommands];
    } while (numExecutedCommands != 0);

    [self.webView stringByEvaluatingJavaScriptFromString:@"cordova.commandQueueFlushing = false"];
}

ということで

  • iframe fire
  • big size data support
  • queuing

の3つを実装することでより優れたJS-Native Bridgeがつくれるでしょう!

その他

callbackのやり方

JSONで渡すデータ内にcallbackようのJSコード文字列を渡してevalしてもらうとか.他色々

画像などのバイナリデータの渡し方

Base64エンコードしてJSONにつっこむ