WKWebViewでローカルのindex.htmlを読み込んでscriptを実行してみた

ネットを介してのWebView表示ではなく、手元にあるHTMLをWebViewで表示、scriptタグ内のJSメソッドの呼び出し方法をまとめます。

SwiftでWebViewを表示させる場合、WKWebViewを使うことがほとんどだと思います。現在ではUIWebViewは非推奨です。そちらについては以下の記事がとても参考になります。
[参考元]：[UIWebViewを使わない理由とWKWebViewを使う理由](https://qiita.com/ShingoFukuyama/items/01a9cdf24090931b4ef3)

## 環境
OS: Mojave 10.14.5
Xcode: 10.1
Swift 4.2

## HTML
今回はindex.htmlをWKWebViewでロードし、`<script>`タグ内にあるJSメソッドを呼び出すことをします。
HTMLファイルはプロジェクト内に置きます。置き場は自由で構わないのですが、例として以下のように配置しておきます。

```:例
project
├─Resource
│ ├index.html
│ └sample.json
│
└─View
  └SomeViewController
```

## HTMLのロード
ローカルのHTMLファイルはパスを取得する必要があり、取得には`Bundle.main.path`を使用します。
仕様では`Bundle.main.path`はプロジェクトの`Build Phases` > `Copy Bundle Resources`にあるファイルを取得するようです。
まずこの項目を確認し、取得したいHTMLファイルがなければ追加しておきましょう。

<img width=500 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139404/baef83dc-2f10-2d11-b951-d9f267541057.png">

[参考元]：[Swiftでプロジェクト内のファイルを取得する際の注意点2つ](https://yuu.1000quu.com/points_to_note_when_acquiring_files_with_swift)

HTMLファイルのパスをURL型にキャストしてWebViewの`loadFileURL`の引数に与えます。これでWebViewにロードできました。

```swift
func loadLocalHTML() {
    guard let path: String = Bundle.main.path(forResource: "index", ofType: "html") else { return }
    let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
    webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
}
```


## Swift側からscript内のjsを実行

### 実行タイミング
`<script>`タグにあるメソッドは、HTML全てがロードし終わってから呼び出さないとメソッド呼び出しに失敗します。
ロードが終わったことを通知するdelegateが`WKNavigationDelegate`にあります。

delegateの通知先は自分であることの宣言を忘れずに。

```swift:SomeViewContoller
webView.navigationDelegate = self
```

```swift:SomeViewController

extension SomeViewController: WKNavigationDelegate {

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // ここでJSの実行をする
    }
}
```

### JSの実行
`evaluateJavaScript`を使います。引数の`javaScriptString`に、実行するメソッドを`String`で渡します。
また、ここの`String`ではjsの文法で書く必要があります。jsの知見が乏しい筆者はここで躓きました。

[公式リファレンス]：[evaluateJavaScript(_:completionHandler:)](https://developer.apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript)

`<script>`の中身は以下のようなものと仮定して、initHTML関数に引数を渡し実行する方法を解説します。

```HTML
<script>
    function initHTML(id, jsonData, onload) {
        console.log('initHTMLだお');
        console.log(id, jsonData);
        onload();
    }
</script>
```

#### 引数
Swiftは静的型付け、JSは動的型付けなので、ゆるい方向への値渡しです。
initHTMLの引数(id, jsonData, onload)は、Swift側ではそれぞれInt,Json,関数として渡します。

基本は文字列リテラル内での値の展開`\()`を使います。
Jsonに関しては、Jsonファイルを読み取って`Data`->`String`に変換して、値展開で渡しています。Stringで渡してもWebView側でJsonとして扱ってくれます。
関数はjsの書き方であるアロー関数を用いて書きましょう。

```swift
extension SomeViewController: WKNavigationDelegate {

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        let intValue = 10
        guard let json = getLocalJson() else { return }
        guard let strJson = String(data: json, encoding: .utf8) else { return }
        let script = "initHTML(\(intValue), \(strJson), () => { console.log('Swiftから渡された値だお') });"
        webView.evaluateJavaScript(script) { object, error in
            print(error ?? "成功")
        }
    }

    func getLocalJson() -> Data? {
        guard let path = Bundle.main.path(forResource: "sample", ofType: "json") else { return nil }
        let url = URL(fileURLWithPath: path)
        let json: Data?

        do { json = try Data(contentsOf: url) }
        catch { json = nil }

        return json
    }
}
```

## コンソールの確認
JSの実行ができたらコンソールを確認しましょう。
しかしWebViewで実行したJSコンソールログはXcodeでは見れません。
ブラウザである`Safari`で見ることができます。`開発` -> `端末名` -> `index.js`と選択するとWebインスペクターが開きます。

![webinspector.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139404/1e655d9b-61bd-d622-aff1-7bddb3477040.png)

<img height="600"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139404/ec024a6e-8466-eaa7-ea2b-9eadd3f030f5.png">

呼び出しがうまくいってログが出力されています！
表示がない場合は左上らへんにある更新マークを押しましょう。

## 次回
WebViewのJS内でprojectのソース（画像とか）にアクセスしたときに発生するエラー、`Orijin null is not byAccess-Control-Allow-Origin`編に続く...

## WWDC19あとがき
SwiftUIめっちゃすごい。iOSのUI回りに激震が走りましたね。
~~ここまでUIの作成ハードルが下がると我々iOSエンジニアの価値が相対的に下がっていくのでは…~~
Combine frameworkの登場により、Rxライブラリは今後必要なくなるのかな。ただObserverパターンの考え方は必要なのでここはそれなりにハードルは依然高そう。

## 参考
[iOS WKWebView ネイティブとローカルJavascript連携](https://qiita.com/beaa/items/cfb80790d333c2da252b)