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

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

# JS側の実装
Firebaseドキュメントの「[iOS の WebView でアナリティクスを使用する](https://firebase.google.com/docs/analytics/ios/webview?hl=ja)」を見ればOKです。

```Javascript: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になったことで、上記のドキュメントが少し古くなっています。
今回は特定のイベントのみを送る仕様にしたので、以下のように実装するとうまくいきました。

```Swift: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の設定に、以下の処理を追加します。

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

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

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

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

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

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

```Swift
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`という名前が使われていると、いらないデータも一緒に送られてくる可能性があります。
その場合、メッセージハンドラ名を変えるか、あるいは上記の実装例のように

```Swift
if name != "event_name" { return }
```

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