Swift
Firebase

【Swift4】FirebaseでWebViewを計測する方法と処理の流れ

iOSアプリからWKWebViewでサイトにアクセスしたときに、JSからアプリ側にデータを渡し、計測を上げるための実装をしてみました。

JS側の実装

Firebaseドキュメントの「iOS の WebView でアナリティクスを使用する」を見ればOKです。

sample.js
function logEvent(name, params) {
  if (!name) {
    return;
  }

  if (window.AnalyticsWebInterface) {
    // Call Android interface
    window.AnalyticsWebInterface.logEvent(name, JSON.stringify(params));
  } else if (window.webkit
      && window.webkit.messageHandlers
      && window.webkit.messageHandlers.firebase) {
    // Call iOS interface
    var message = {
      command: 'logEvent',
      name: name,
      parameters: params
    };
    window.webkit.messageHandlers.firebase.postMessage(message);
  } else {
    // No Android or iOS interface found
    console.log("No native APIs found.");
  }
}

function setUserProperty(name, value) {
  if (!name || !value) {
    return;
  }

  if (window.AnalyticsWebInterface) {
    // Call Android interface
    window.AnalyticsWebInterface.setUserProperty(name, value);
  } else if (window.webkit
      && window.webkit.messageHandlers
      && window.webkit.messageHandlers.firebase) {
    // Call iOS interface
    var message = {
      command: 'setUserProperty',
      name: name,
      value: value
   };
    window.webkit.messageHandlers.firebase.postMessage(message);
  } else {
    // No Android or iOS interface found
    console.log("No native APIs found.");
  }
}

ネイティブ側の実装

Swift4になったことで、上記のドキュメントが少し古くなっています。
今回は特定のイベントのみを送る仕様にしたので、以下のように実装するとうまくいきました。

sample.swift
func userContentController(_ userContentController: WKUserContentController,
                               didReceive message: WKScriptMessage) {
        guard let body = message.body as? [String: Any] else { return }
        guard let command = body["command"] as? String else { return }
        guard let name = body["name"] as? String else { return }

        if command == "logEvent" {
            guard let params = body["parameters"] as? [String: NSObject] else { return }
            if name != "event_name" { return }
            Analytics.logEvent(name, parameters: params)
        }
    }

userContentController関数を使うには、WKScriptMessageHandlerプロトコルが必須なので忘れないようにしましょう。

また、loadViewで行ったWKWebViewの設定に、以下の処理を追加します。

self.webView.configuration.userContentController.add(self, name: "firebase")

何をやっているのか?

これで実装はできるのですが、一体JSとWebViewの間ではどんなやり取りがされているのでしょうか。コードに書かれた処理の流れをもう少し詳しく見てみます。

JS側の処理

まず、サイト上でイベントが起こると、logEvent関数またはsetUserProperty関数が呼び出されます。(いつどの関数が呼び出されるかは、自分で実装しなければいけません)
これらの関数は、messageという変数の中に、イベントの名前やパラメータを荷詰めします。
そして、

window.webkit.messageHandlers.firebase.postMessage(message);

という処理が肝です。この処理によって、messagefirebaseという名前のメッセージハンドラでネイティブに送られます。

ネイティブ側での準備

ネイティブ側では、あらかじめJSからの通信を受け取る準備をしておく必要があります。それが

self.webView.configuration.userContentController.add(self, name: "firebase")

の部分です。
ここに使われているのは、WKUserContentControllerオブジェクトです。これはJSからmessageを受け取る手段を提供してくれるものです。
ここに、JS側で指定したメッセージハンドラを追加しておく必要があります。JSの側でfirebaseという名前にしたので、ネイティブ側でもfirebaseという名前にする必要があります。でないと何も受け取れません。

ネイティブ側の処理

ネイティブ側にmessageが届くと、userContentController関数が呼び出されます。
ここで届いたmessageから、command, nameなどの情報を取り出します。
上記コードでは、JS側でcommandの値によってmessageのキー構成が異なるので、ネイティブ側でcommandの値によって処理を分けています。もし必要な情報が入っていなければ何もせずに関数から抜けるようになっています。
そして情報を取り出し終わると、またはでFirebaseに送ります。これでようやく、Firebaseに計測が上がります。

メッセージハンドラ名の重複に注意

今回はメッセージハンドラ名にfirebaseを使いましたが、既に他のデータを送るためにfirebaseという名前が使われていると、いらないデータも一緒に送られてくる可能性があります。
その場合、メッセージハンドラ名を変えるか、あるいは上記の実装例のように

if name != "event_name" { return }

というような、ほしいイベント以外のデータを排除する処理を付け加えると無難でしょう。