iOSでガワネイティブでWebView上で動かしているJSとSwiftの基本的なやりとりを試したけど大事なことが抜けていた。件の記事でJSからSwiftを呼び出してあとはネイティブに任せっきりという処理はできるが、ネイティブからなにか情報を取得することを試していなかった。

JSからネイティブを呼び出し結果を受け取る方法

webView:shouldStartLoadWithRequestではリクエストされたURLに遷移するかどうかは制御できるが、値の取得はできない。値を取得するには別の仕組み、NSURLProtocolというものを使う。

NSURLProtocolを継承したクラスを作りシステムに登録するとURLリクエストのたびにcanInitWithRequestが呼び出されるので、フックしたい場合にはtrueを返してその後呼び出されるstartLoadingでレスポンスを返すとブラウザに渡される。JSからはAjaxでフックされるURLを叩いて結果を得るようにすればネイティブから値を取得することができる。

カスタムのURLProtocolクラスを定義する

// MyURLProtocol.swift
import UIKit

private let HOOK_HOST = "native";

class MyURLProtocol: NSURLProtocol {
  override class func canInitWithRequest(request: NSURLRequest) -> Bool {
    // "http://native/..."のリクエストをフック
    if request.URL.scheme == "http" && request.URL.host == HOOK_HOST {
      return true
    }
    return false
  }

  override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
    return request;
  }

  override func startLoading() {
    let url = request.URL
    // URLによってなにかレスポンスを返す(後述)
  }

  override func stopLoading() {
    // 特に何もしない
  }
}

作ったURLProtocolを登録する

ViewControllerviewDidLoadかなにかに以下を追加すると、すべてのWebViewのURLリクエストがフックされる:

    NSURLProtocol.registerClass(MyURLProtocol)

アプリの終了などで使わなくったらunregisterしてやること:

    NSURLProtocol.unregisterClass(MyURLProtocol)

JavaScriptからの呼び出し

通常のAjaxのようにXmlHttpRequestを使うが、違いとしてはAsyncじゃなく同期にしてやると違和感がないのではないかと思う(JS的には結局コールバックを使うことになるが…):

// test.js
function foobar(url) {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (200 <= xhr.status && xhr.status < 300) {
      // 成功
      var result = xhr.responseText;  // Swiftから返された内容
    } else if (xhr.status >= 400) {
      // 失敗
    }
  };
  xhr.open('GET', url, false);  // 第3引数=asyncかどうか
  xhr.send(null);
}

foobar('http://native/foo?bar=baz');

カスタムのURLProtocolでリクエストに対する結果を返す

自分で定義したMyURLProtocolstartLoadingでリクエストに対してクライアントになにか結果を返してやる必要がる。簡単に返せるように、sendBackEmptysendBackPlainTextというメソッドを用意してやった:

// MyURLProtocol.swift
class MyURLProtocol: NSURLProtocol {
  ...

  // ダミーを返す場合
  private func sendBackEmpty() {
    let response = NSURLResponse(URL: self.request.URL,
      MIMEType: nil,
      expectedContentLength: 0,
      textEncodingName: nil)
    self.client?.URLProtocol(self,
      didReceiveResponse: response,
      cacheStoragePolicy: NSURLCacheStoragePolicy.NotAllowed)
    self.client?.URLProtocol(self, didLoadData: NSData(bytes: nil, length: 0))
    self.client?.URLProtocolDidFinishLoading(self)
  }

  // プレーンテキストを返す場合
  private func sendBackPlainText(str: String) {
    if let data: NSData = str.dataUsingEncoding(NSUTF8StringEncoding) {
      let headers = [
        "Content-Type": "text/plain",
        "ContentLength": "\(data.length)",
      ]
      let response = NSHTTPURLResponse(URL: self.request.URL,
        statusCode: 200,
        HTTPVersion: "1.1",
        headerFields: headers)
      self.client?.URLProtocol(self,
        didReceiveResponse: response!,
        cacheStoragePolicy: NSURLCacheStoragePolicy.NotAllowed)
      self.client?.URLProtocol(self, didLoadData: data)
      self.client?.URLProtocolDidFinishLoading(self)
    }
  }
}