[SwiftUI] WebViewからのJSイベントをSwift側で検知する

WebView内で表示されている画像をクリックすると、その画像が全画面で表示されるようにしたいというタスクがありました。
WebView内で発生したイベント(画像クリック)はSwift側で検知することができるようでしたので、今回は簡単に処理の流れを書いていこうと思います。

### imgタグにクリックイベントを貼る
下記のようにUIViewRepresentableを継承したWebViewを表示する為のWebViewerがあるとします。(※分かりやすいように処理の一部を抜粋してきています。このコードだけでWebViewが動作する訳ではありません)

```swift
struct WebViewer: UIViewRepresentable {
  func makeUIView(context: Context) -> WKWebView {
    let webViewConfig = WKWebViewConfiguration()
    let webView = WKWebView(frame: .zero, configuration: webViewConfig)
    webView.navigationDelegate = context.coordinator
    return webView
  }
  
  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
  
  class Coordinator: NSObject, WKNavigationDelegate {
    var parent: WebViewer
    
    init(_ parent: WebViewer) {
      self.parent = parent
    }
  }
}
```
ではmakeUIView関数の中に下記の記述を足しましょう。imgAddClickListenerScriptの中身は見ての通り、全てのimgタグに対してクリックイベントをセットしています。
```swift
    let imgAddClickListenerScript = """
    document.querySelectorAll('img').forEach(function(img) {
      img.addEventListener('click', function(event) {
          window.webkit.messageHandlers.imageClick.postMessage(img.src);
      });
    });
    """
```
イベントの中の記述に下記のような記述があります。
```swift
window.webkit.messageHandlers.imageClick.postMessage(img.src);
```
これはWebView内のJSからアプリ側に対してメッセージを送信するための記述になります。imageClickという部分はSwift側でイベントを受け取る際のメッセージハンドラー名になります。これを後ほど記載するWKUserContentControllerを通して登録することで、イベント発生時にSwift側でこの名前を指定し、イベントが受け取れるようになるという訳です。ここの名前は自由に決めることができますので、イベント名に沿ったメッセージ名とすると分かりやすいです。

続いて下記のコードをimgAddClickListenerScriptの下に足します。

```swift
let userScript = WKUserScript(source: imgAddClickListenerScript, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
let userContentController = WKUserContentController()
// 用意したScriptをコントローラー側に設定
userContentController.addUserScript(userScript)
userContentController.add(context.coordinator, name: "imageClick")
webViewConfig.userContentController = userContentController
```
下記に要点を書いていきます。
・WKUserScript
WebView側でJSを実行する為のクラスになります。ここに先ほど用意したimgAddClickListenerScriptをセットします。

・WKUserContentController
用意した上記のWKUserScriptの注入や、JSからSwift側へのメッセージの送信設定などを行います。userContentController.add(context.coordinator, name: "imageClick")という部分が、
上記で述べたメッセージハンドラー名を登録する処理です。WebView内でのイベントはCoordinatorクラス(UIkit)側で検知することになりますので、第一引数にCoordinatorオブジェクトを指定しています。

それではイベントをセットすることができたので、実際に検知する側の処理を書いていきましょう。
```swift
  class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
    var parent: WebViewer
    
    init(_ parent: WebViewer) {
      self.parent = parent
    }

        /// WebView内で発生したJSのイベント通知を受け取る
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
      if message.name == "imageClick", let imageUrl = message.body as? String {
        print("click:", imageUrl)
      }
    }
  }
```
Coordinatorクラスに新たにWKScriptMessageHandlerというプロトコルを継承させるようにしました。これはWebKitフレームワークにおけるプロトコルで、WebView内で実行されるJSからのメッセージを受信する為のものです。
userContentControllerという関数は上記のプロトコルに定義されているメソッドで、JSからメッセージが送信されると自動で呼び出されます。上記でセットしたイベントによって、画像をクリックするとimageClickという名前のメッセージハンドラーが飛んできますので、名前がimageClickだったらクリックした画像URLをprintで出力するという動きになります。
あとはSwiftUI側でこの画像URLを元に全画面表示を実装するだけとなります。

## 終わりに
以上がWebView内からSwift側にイベント通知する方法です。同じようなケースを実装する機会があればぜひ参考にしてみてください。