WKWebViewと向き合ってみた

UIWebViewがiOS8から"deprecated"になり、WKWebViewの使用が推奨されている。

> **Important**
> 
> Starting in iOS 8.0 and OS X 10.10, use WKWebView to add web content to your app. Do not use UIWebView or WebView.

WKWebViewを使用するにあたって色々調べてみたところ、UIWebViewと比較しWKWebViewの優位性として、

* 安定性 (クラッシュ率の低減。メモリ領域の変更。)
* 実行速度 (JavaScriptベンチマークで約10倍の結果。)
* セキュリティ (よくわからないが、接続先, 元の操作が不可能になった、など。)
* 機能性 (IndexedDBが有効になった、など。)

など、どれをとってもWKWebViewを使わない手はないといった感じである。実際、iOS版Google Chromeを始め、iOS版Firefoxといったブラウザアプリも早い段階でUIWebViewからWKWebViewに移行済みだ。

iOS8時点では、WKWebViewも機能不足な面が多かったが、iOS9にはその多くが改善され、さらに新たな機能も盛り込まれており、将来性も高い。

本記事では、そんなWKWebViewを実際に商業用プロジェクトで使ってみた所見を述べようと思う。

## Storyboardにおける問題
Xcode9以降かつDeployment Target iOS11以上でなければ、Storyboard上で設定するとこができない。これは、iOS SDK11未満のWKWebViewのNSCordingにバグが存在していたためである。(Xcode9以降であれば、UIコンポーネント自体は存在するがエラーになる。) かのAppleが3世代もこのバグを放置していたという事実に驚きである。
というわけで、iOS11未満もサポートする場合はコードでWKWebViewを実装する必要がある。(厳密にはOS毎にStoryboardを出し分けるといったことも可能であるが、素直にコードで実装するのがいいだろう。)

## ネイティブ側からWebViewのJavaScriptを実行
早速ネガティブ所見から入ってしまったが、ここからがWKWebViewの本懐。

ただ単純にJavaScriptを実行するだけでいいのであれば、

```Swift
let javaScriptString = "document.getElementById('hoge').innerHTML = 'fuga'";
webView.evaluateJavaScript(javaScriptString, completionHandler: nil)
```

でおｋ。コレはUIWebViewにも同様のメソッドが用意されている。

では、WKWebViewでは何ができるようになっているか。

WKWebViewでは、JavaScriptの実行をドキュメントロード開始時、もしくは終了時を指定して実行することができるのである。UIWebViewでは、最速でJavaScriptを実行したい場合でも、デレゲートメソッドである"webViewDidFinishLoad(_:)"をフックに、"stringByEvaluatingJavaScriptFromString:"によってJavaScriptを実行するほかないので、どうしても描画後にJavaScriptの実行結果が反映されることになってしまう。WKWebViewではドキュメントロード開始時にJavaScriptを実行できるので、JavaScriptの実行結果が反映された状態で描画されるので、より自然なUXを提供できることだろう。

```Swift
let javaScriptString = "document.getElementById('hoge').innerHTML = 'fuga'";
let userScript = WKUserScript(source: javaScriptString, injectionTime: .atDocumentStart, forMainFrameOnly: true)

let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)

let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.userContentController = userContentController

let webView = WKWebView(frame: self.view.bounds, configuration: webViewConfiguration)
self.view.addSubview(webView)	
```

## WebView側からネイティブのメソッドを実行
ココが今回一番WKWebViewを使ってみて、一番感動したポイント。

UIWebViewでは、"location.href"でschemeを設定したURLをロードし、"webView(_:shouldStartLoadWith:navigationType:)"でschemeを判定してフックするといった手法が一般的かと思われる。この方法だと、URLであるschemeで定義できる仕様に制限があるし、直感的ではないと筆者はかねがね感じていた。

WKWebViewでは、コールバックメソッドを設定することができるようになっており、かなり直感的にネイティブのメソッドを実行できるようになっている。

WebView側からは、"window.webkit.messageHandlers"でコールバックメソッドを呼ぶことになるが、この時引数で値渡しが可能となっている。引数の値をjson形式などにしておくと、ネイティブ側でそのままパースして使用することができるのでオススメである。

``` Swift:ViewController
let userContentController = WKUserContentController()
// WebView側からのコールバックメソッドを設定 (本サンプルでは"hoge"メソッド)
userContentController.add(self, name: "hoge")

let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.userContentController = userContentController

let webView = WKWebView(frame: self.view.bounds, configuration: webViewConfiguration)
self.view.addSubview(webView)
```

``` Swift:ViewController
extension ViewController: WKScriptMessageHandler {
    
    /// WebView側からメソッドがコールバックされたときに呼ばれる
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    	if "hoge" == message.name {
            // "hoge"メソッドの処理
            if let json = message.body as? String {
                // WebView側の"postMessage"の引数で渡された値
            }
        }
    }
        
}
```

``` javascript:javascript
window.webkit.messageHandlers.hoge.postMessage(json);
```


## Cookie管理
iOS11で追加されたWKHTTPCookieStoreにより、Cookieの追加, 削除、Cookieの変更の監視などが容易になった。が、iOS11未満ではWKWebViewにおけるCookieはお世辞にも扱いやすいとは言いづらい。

```Swift
// リクエスト毎にJavaScriptでCookieをセットすることで永続化させている
let userScript = WKUserScript(source: "document.cookie='hoge=fuga; path=/';", injectionTime: .atDocumentStart, forMainFrameOnly: true)

let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)

let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.userContentController = userContentController

let webView = WKWebView(frame: self.view.bounds, configuration: webViewConfiguration)
self.view.addSubview(webView)

// 初回リクエスト時はリクエストにCookieを設定する
let url = URL(string: "http://hoge.com")
var urlRequest = URLRequest(url: url!)
urlRequest.addValue("hoge=fuga;", forHTTPHeaderField: "Cookie")
webView.load(urlRequest)
```

## 終わりに
使い慣れたUIWebViewと違い、最初はWKWebViewを敬遠しがちだったが、いざ使ってみるとUIWebViewと比較しても優位な点が多く、非常に印象は良かった。ただ、変なバグがあったり、使いにくいところもあったが、改善スピードも非常に速いので将来性も高いだろうという感想である。