「The Ultimate Guide to WKWebView」をSwiftUIで実装する #10 - Injecting JavaScript into a page -

[「The Ultimate Guide to WKWebView」](https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebvie)をSwiftUIで実装してみるの、
１０こ目になります。

「[#02](https://qiita.com/kamimi01/items/c889979964820d5f9fa2)」の記事以来の、JavaScriptとのご対面です。

## 目次

シリーズ化していこうと思うので、全体の目次を置いておきます。
リンクが貼られていないアジェンダは、記事作成中または未作成のものになります。

|#|タイトル|
|------|-----------------|
|01|[Making a web view fill the screen](https://qiita.com/kamimi01/items/b4d6cbafa587831c1be0) <br> （WebViewを画面に表示する）|
|02|[Loading remote content](https://qiita.com/kamimi01/items/c889979964820d5f9fa2) <br> （リモートのコンテンツを読み込む）|
|03|[Loading local content](https://qiita.com/kamimi01/items/9cefe1e73ab5b88051c6) <br> （ローカルのコンテンツを読み込む）|
|04|[Loading HTML fragments](https://qiita.com/kamimi01/items/2f267b328f3f6158bc26) <br> （HTMLフラグメントの読み込み）|
|05|[Controlling which sites can be visited](https://qiita.com/kamimi01/items/7c01aed497108da35b5a) <br> （訪問可能なサイトの制御）|
|06|[Opening a link in the external browser](https://qiita.com/kamimi01/items/716dccb6f109ee50c900) <br> （外部ブラウザでリンクを開く）|
|07|[Monitoring page loads](https://qiita.com/kamimi01/items/33446e032822518cb7f7) <br> （ページの読み込みを監視する）|
|08|[Reading a web page’s title as it changes](https://qiita.com/kamimi01/items/7e0153ee68474a155b9a) <br> （Webページのタイトルの変化を読み取る）|
|09|[Reading pages the user has visited](https://qiita.com/kamimi01/items/f5f6c35a6a68726fa485) <br> （ユーザーが閲覧したページを読み取る）|
|10|[Injecting JavaScript into a page](https://qiita.com/kamimi01/items/6076081dc54e2b3d0abf) <br> （JavaScriptをページに注入する）|
|11|[Reading and deleting cookies](https://qiita.com/kamimi01/items/90139fe692ec988d28bb) <br> （cookieの読み取りと削除）|
|12|[Providing a custom user agent](https://qiita.com/kamimi01/items/5884330e315338df8ec0) <br> （カスタムUser Agentを提供する）|
|13|[Showing custom UI ](https://qiita.com/kamimi01/items/3315e4df0813eda75eeb)<br> （カスタムUIを表示する）|
|14|Snapshot part of the page <br> （ページの一部のスナップショットを撮る）|
|15|Detecting data <br> （データの探索）|

## 環境

【Xcode】13.1
【Swift】5.5
【iOS】15.0
【macOS】Big Sur バージョン 11.4

## 実現したいこと

今回やることは、
まずiOSアプリ側で値を入力し、WebViewを開くときにその値を表示するHTMLに注入します。

とこれだけ書いてGIF画像を見ると、#02と変わらないように見えるのですが、
実現方法が以前と違います。

「#02」の時は、サーバーに値を送信し、サーバー側でHTMLに値を注入していたのですが
今回はサーバーには送らず、ネイティブ側でJavaScriptの関数を実行し、HTMLに値を注入します。

以下が実際の動きです。

![app](https://user-images.githubusercontent.com/47489629/157545108-0a27b18e-9cba-41d0-b0fd-0286018a8166.gif)

また[参考](#参考)にもリンクを記載していますが、
今回のアプリの挙動は、「[WKWebView and JavaScript interaction](https://medium.com/@mushtaque87/webkit-4bd5cf3dc08)」を参考にしております。感謝。:pray:

実務で使ったことなかったから、どういうのがよくある使用例なのか調べるのに時間かかった・・・
まあこれも実際よく使われているのかは私にはわからないわけですが、そこは将来の自分がなんとかしてくれるはず。笑

## 実現方法

まずWebViewです。

少し長いのですが、**メインは`webView(_:didFinish:)`メソッドにある`evaluateJavaScript(_:completionHandler:)`** です。

Webページの読み込みが終了したら、JavaScriptを実行しています。
その下にある`createJsonForJavaScript`メソッドは、JavaScript実行時に必要となるJSON文字列を作成しています。

```swift:WebView.swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let url: URL
    let name: String
    let email: String
    private let webView = WKWebView()

    func makeUIView(context: Context) -> WKWebView {
        webView.navigationDelegate = context.coordinator
        let request = URLRequest(url: url)
        webView.load(request)
        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
}

extension WebView {
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: WebView

        init(_ parent: WebView) {
            self.parent = parent
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            let parameters = ["name": parent.name, "email": parent.email]
            guard let jsonString = createJsonForJavaScript(for: parameters) else {
                return
            }
            let jsString = "fillDetails('\(jsonString)')"
            // JavaScriptのメソッドを実行する
            parent.webView.evaluateJavaScript(jsString) { value, error in
                print(value)  // 「executed　javascript method」が表示される
                print(error?.localizedDescription)  // 成功した場合は、nilになる
            }
        }
        
        // {  \"email\" : \"mailmail\",  \"name\" : \"Mika\"} のようなJSON文字列を生成する
        private func createJsonForJavaScript(for data: [String: Any]) -> String? {
            var jsonString: String?

            do {
                let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
                jsonString = String(data: jsonData, encoding: .utf8)

                jsonString = jsonString?.replacingOccurrences(of: "\n", with: "")
            } catch {
                print(error.localizedDescription)
            }

            print(jsonString)

            return jsonString
        }
    }
}
```

もう少し詳しくみていきます。

まず、[`evaluateJavaScript(_:completionHandler:)`](https://developer.apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript)についてです。

これは、名前の通り、JavaScriptを評価するためのメソッドです。
引数は、以下の2つあります。

- `javaScriptString`
- `completionHandler`

まず第一引数`javaScriptString`は、わかりやすいですね。**実行したいJavaScript文字列**です。

もし実行したいJavaScriptが長い場合は、別ファイルとしてアプリバンドル内に配置して、
こんな感じに↓読み込んでくる方法もあります。（コードは「[WKWebViewを使ってNativeとWebページで情報をやり取りする方法](https://www.mitsue.co.jp/knowledge/blog/apps/201605/30_1721.html)」から引用させていただきました。）
ですが今回はその方法は使っていません。

```swift
var jsSource = ""
// JavaScriptコードはtest.jsという名前で別ファイルに用意
let path = NSBundle.mainBundle().pathForResource("test", ofType: "js")!
if let data = NSData(contentsOfFile: path){
  jsSource = String(NSString(data: data, encoding: NSUTF8StringEncoding)!)
}
```

次に`completionHandler`です。

公式ドキュメントによると、

「JavaScriptの評価が終わったら、実行されるブロック。メソッドはJavaScriptの評価の成功・失敗に関わらず、ブロックを呼び出す。ブロックは戻り値を持たず、次のパラメータを受け取る。
`object`：JavaScriptの評価の結果。エラーが発生した場合は`nil`
`error`:成功した場合は、nil。または問題の情報を持ったエラーオブジェクト。
」

> A handler block to execute when script evaluation finishes. The method calls your block whether script evaluation completes successfully or fails. The block has no return value and takes the following parameters:
object
The result of the script evaluation, or nil if an error occurred.
error
nil on success, or an error object with information about the problem.

とのこと。

今回は、こんなJavaScriptのメソッドを用意してこれをネイティブ側から実行しているので、問題なくJavaScriptの実行が成功すれば
`object`としては、戻り値である`executed　javascript method`の文字列を受け取り、
`error`は`nil`となっています。

```javascript:index.js
function fillDetails(jsonData) {
  console.log("jsonData: ", jsonData);
  
  let userData = JSON.parse(jsonData);

  document.getElementById("name").value = userData["name"];
  document.getElementById("email").value = userData["email"];

  return "executed　javascript method"
}
```

ちなみにHTMLはこうです。
テキストフィールドが２つ並んでいるだけのシンプルなページです。

```html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" id="name">
    <br>
    <input type="text" id="email">
    <script type="text/javascript" src="http://localhost:3000/js"></script>
  </body>
</html>
```

また補足ですが、
JavaScriptを実行できるメソッドは、`evaluateJavaScript(_:completionHandler:)`以外にもあるようです。

それが[`WKUserScript`クラス](https://developer.apple.com/documentation/webkit/wkuserscript)のメソッド。
`evaluateJavaScript(_:completionHandler:)`と違い、**どのようにJavaScriptを実行するのか、どのタイミングで実行するのかを制御できる**そうです。

今回は取り上げません・・・ :pray:

---

では最後に、WebViewを使用する初期表示画面のViewです。
テキストフィールドが２つとWebViewを表示するためのボタンが１つ置いてあります。

```swift:Content.View
import SwiftUI

struct ContentView: View {
    private let url = URL(string: "http://localhost:3000/")!
    @State private var isShownWebView = false
    @State private var name = ""
    @State private var email = ""

    var body: some View {
        VStack {
            TextField("name", text: $name)
                .textInputAutocapitalization(.never)
                .frame(width: 200)
                .border(.yellow)
            TextField("email", text: $email)
                .textInputAutocapitalization(.never)
                .frame(width: 200)
                .border(.yellow)
            Button(action: {
                isShownWebView.toggle()
            }) {
                Text("値を送信してWebViewを開く")
            }
        }
        .fullScreenCover(isPresented: $isShownWebView) {
            WebView(url: url, name: name, email: email)
        }
    }
}
```

テキストフィールドに入力したとき、デフォルトだとアルファベットの最初の文字が大文字になってしまうので、
それを防ぐために、[`textInputAutocapitalization(_:)`](https://developer.apple.com/documentation/swiftui/menu/textinputautocapitalization(_:))を使用しています。

iOS15より前では、[`autocapitalization(_:)`](https://developer.apple.com/documentation/swiftui/menu/autocapitalization(_:))だったのですが、Deprecatedになっていますね。iOS15より前をサポートしないなら、`textInputAutocapitalization(_:)`を使った方が良いようです。

iOSの実装としては以上です！

「The Ultimate Guide to WKWebView」にはないですが、`WKUserScript`も触ってみたいです。:blush:

コード全体は以下にあがっています。

https://github.com/kamimi01/TheUltimateGuidetoWKWebViewWithSwiftUI/tree/main/InjectingJavaScriptIntoAPage

## その他調べたこと

WebViewやSwiftUI自体とは関係ないですが、調べたこと、やったことを書いておきます。

### APIの用意

大したことはしていないのですが、、
index2.htmlを、`http://localhost:3000/`で
index2.jsを、`http://localhost:3000/js`で
呼び出せるようにしています。

```js:index.js
const express = require("express");
const app = express();

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index2.html')
});

app.get('/js', (req, res) => {
  res.sendFile(__dirname + '/index2.js')
});

// ポート3000番でlistenする
app.listen(3000);
```

## 参考

- [WKWebView and JavaScript interaction](https://medium.com/@mushtaque87/webkit-4bd5cf3dc08)
- [WKWebViewを使ってNativeとWebページで情報をやり取りする方法](https://www.mitsue.co.jp/knowledge/blog/apps/201605/30_1721.html)
- [`evaluateJavaScript(_:completionHandler:)`Apple公式ドキュメント](https://developer.apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript)
- [`WKUserScript`Apple公式ドキュメント](https://developer.apple.com/documentation/webkit/wkuserscript)
- [WKWebView で JS を使用して値を取得する 3 つの方法](https://qiita.com/yuki0n0/items/0e85846e029fb7353224)
- [WKWebViewを使ってNativeとWebページで情報をやり取りする方法](https://www.mitsue.co.jp/knowledge/blog/apps/201605/30_1721.html)