WKWebViewに舐めてかかって返り討ちにあったアレコレ

## 経緯
UIWebViewの使用経験が長かったこともあり、WKWebViewを使う際にそんな苦労しないし余裕でしょ！って、舐めてかかったら痛い目を見たので今更な内容ではあるが、自戒を込めてまとめました。

正直、ちゃんとドキュメント読めばいくつかは最初から気づけたので、ちゃんと読めという話です。

## 開発環境
* Xcode 9.2
* Swift 4.0
* iOS Deployment Target 9.0
* 画面はStoryboardで実装できる箇所は全てStoryboardに寄せて作成
* WKWebViewだけはコード実装
* WhiteList(配列に持っているドメイン名)の文字列のみ、WKWebViewで表示するのを許可し、他はSafariに飛ばす仕様

## 1. Error用Delegateメソッドを1つしか実装していなかった
### 発生した問題
* Error用のDelegateメソッドにBreak Pointを張っていたが、端末を機内モードにしてもBreak Pointで止まらなかった

### 原因
`func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)`しか実装していなかった。

調べたらもう1つError用のDelegateメソッド `func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)`があり、こちらでErrorが流れてくるようになっていた。

#### 補足
ドキュメントを見ると用途が以下のように違うことが記載されている

* `func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)`
	* WebPageの読み込み途中でerrorが起きた時に呼ばれる
		* 発生例  -> ページ読み込み中にリンクをタップして読み込みをキャンセルした時等
* `func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)`
	* WebPageの読み込み開始時にerrorが起きた時に呼ばれる
		* 発生例 -> 圏外によりページを読み込めたかった時等

エラーの発生状況でどちらのDelegateメソッドにもErrorが来る可能性はあるため、両方実装した上で適切にハンドリングしてやる必要がある

## 2. ローカルのError時用のHTMLを読み込んだらreloadで復帰できなくなった
### 発生した問題
* ローカルにあるerror.htmlをエラー発生時にWKWebViewに読み込ませたら、
`goBack()`、`goForward()`時に正常なWebページの合間にerror.htmlが表示されるようになった。
* `reload()`でerror.htmlが繰り返し読み込まれ、error前にURLRequestで投げたページにアクセスしようとしなくなった。

### 原因
ローカルにあるHTMLを読み込む方法は複数あるが、fileプロトコルでHTMLを読み込んでいたため、WKWebViewのHistoryにStackされるようになっていた。

HTMLを文字列としてWKWebViewに読み込ませることで、HistoryにStackさせなくすることができる。

#### 補足
* fileプロトコルで読み込み
  * WebViewのurlに`file:///...`のフォーマットで入る
  * urlがhistoryにstackされ、`reload()`や`goBack()`、`goForward()`で読み出される
  * url.isFileでtrueが返るようになる(ローカルからの読み出し判別が可能)

```swift:sample
let errorHtmlPath = Bundle.main.path(forResource: "error", ofType: "html")!
let url = URL(fileURLWithPath: errorHtmlPath)
webView.load(URLRequest(url: url))
```

* HTMLを読み込み(baseURLにnilを入れる場合のみ)
  * WebViewのurlには`about:blank`が入る
  * historyにstackされないので、`reload()`や`goBack()`、`goForward()`で読み出されない
  * 意図的に`about:blank`を表示した場合と区別つけられない(ローカルのからの読み出し判別が不可能)

```swift:sample
let errorHtmlPath = Bundle.main.path(forResource: "error", ofType: "html")!
let htmlString = try! String(contentsOfFile: errorHtmlPath)
webView.loadHTMLString(htmlString, baseURL: nil)
```

## 3. リンクをタップしても読み込まなかったり、意図せずSafariに飛ばすことになった
### 発生した問題
* リンクをタップしても読み込みを開始しない
* WhiteListに含まれるページにも関わらず、ページ内にあるFacebookやTwitterのボタンのURLがトリガーとなってSafariに遷移する

### 原因
URLの読み込みを許可するか許可しないかを決定できるDelegateメソッド
`func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) `内で行うハンドリングに問題があった。

`navigationAction.navigationType`でリンクをタップした時のURLなのか、`goBack()`、`goForward()`、`reload()`で読み込まれたURLなのか等を判別できるが、一律同じ処理をしていたため問題が発生していた。
(特にリンクタップ時の`linkActivated`とリンクタップ後ページ内にある他の読み込み必要なURLやリダイレクトの時の`other`を同一として扱うが良くなかった。)

#### 補足
最終的には、このようなコードでハンドリングすることになった。

```swift:WebViewController.swift
extension WebViewController: WKNavigationDelegate {

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        // navigationTypeに応じて処理を分岐
        switch navigationAction.navigationType {
        case .linkActivated:
            linkActivated(webView: webView, navigationAction: navigationAction, decisionHandler: decisionHandler)
        case .backForward, .formResubmitted, .formSubmitted, .reload, .other:
            othersAction(webView: webView, navigationAction: navigationAction, decisionHandler: decisionHandler)
        }
    }

    func linkActivated(webView: WKWebView, navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        // リンクをタップした時のみの処理
        // タップした遷移先のURLを取得
        guard let url = navigationAction.request.url else {
            decisionHandler(.cancel)
            return
        }
        // 遷移先をwebviewで表示していいドメインかチェック
        guard allowLoadUrl(url: url) else {
            // WhiteListにないドメインなのでSafariに飛ばす
            UIApplication.shared.open(url)
            decisionHandler(.cancel)
            return
        }
        // WebViewでの表示許可
        decisionHandler(.allow)
    }

    func othersAction(webView: WKWebView, navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        // リンクタップ以外の処理(戻る、進む、リダイレクト等)
        // MainFrameのRequestか判定
        guard let isMainFrame = navigationAction.targetFrame?.isMainFrame, isMainFrame else {
            // リンクタップ時のメインフレーム以外はここに流れるので全て許可(FacebookやTwitterのURL諸々)
            decisionHandler(.allow)
            return
        }
        // リダイレクト等の遷移先のURLを取得
        guard let url = navigationAction.request.url else {
            decisionHandler(.cancel)
            return
        }
        // 遷移先をwebviewで表示していいドメインかチェック
        guard allowLoadUrl(url: url) else {
            // WhiteListにないドメインなのでSafariに飛ばす
            UIApplication.shared.open(url)
            decisionHandler(.cancel)
            return
        }
        // WebViewでの表示許可
        decisionHandler(.allow)
    }
}
```

##### 参考にしたURL：
* https://qiita.com/1234224576/items/0ecee5ce877e99ac3647

## 4. Storyboard上でiPhone 8の4.7inchサイズを表示して作っていたら他の画面サイズの時にWKWebViewのサイズがおかしくなった
### 発生した問題
iPhone 8等の4.7inchサイズの時はWKWebViewが正常に意図通りのサイズで表示されるが、他の画面サイズで動作させた際に、意図しないWKWebViewのサイズで表示された。

### 原因
Storyboard上の表示を`View as: iPhone 8`にしてUIを作っていたため、各Viewの初期サイズがiPhone 8の画面サイズで生成されていた

また、WKWebViewにAutoLayoutの制約をつけ忘れていた。

#### 補足
WKWebViewを下記のように生成と配置をしていた。

```swift:WebViewController.swift
@IBOutlet var webViewBase: UIView
lazy var webView = WKWebView(frame: webViewBase.frame)

func viewDidLoad() {
	super.viewDiDLoad()
	webViewBase.addSubview(webView)
  webView.navigationDelegate = self
} 
```

`lazy var`でwebViewを遅延生成しているものの初回で呼ばれる`viewDidLoad()`でwebViewBaseのサイズを元にwebViewが作られる。
しかし、このタイミングでは、AutoLayoutの制約が反映される前のサイズのため、画面が表示された時には、意図しないサイズでwebViewが表示される。

AutoLayoutの制約が反映されるタイミングは、Lifecycleの`viewDidLayoutSubviews()`で、その前にviewのサイズを取得しても制約により変動する可能性がある。

```swift:Lifecycle
// Lifecycle
viewWillAppear
viewWillLayoutSubviews // AutoLayoutの調整開始前
viewDidLayoutSubviews // AutoLayoutの調整完了
viewDidAppear
```

4.7inch端末で意図通りのサイズで表示されていた理由は、Storyboard上の`View as: iPhone 8`の表示しているサイズに依存していたため。

<img width="1264" alt="スクリーンショット 2018-03-29 18.43.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/154832/7c7c155e-9926-7394-46a6-18dc670eb64f.png">

WKWebViewに制約をついていないことが問題だったので、`viewDidLoad()`で下記のように制約をつけてやることでどの画面サイズでも意図した見た目で表示された。

```swift:WebViewController.swift
// webViewの制約設定時、AutoresizingMaskによって自動生成される制約と競合するため、自動生成をやめる
webView.translatesAutoresizingMaskIntoConstraints = false
// webViewの制約
NSLayoutConstraint.activate([webView.leadingAnchor.constraint(equalTo: webViewBase.leadingAnchor),
                             webView.trailingAnchor.constraint(equalTo: webViewBase.trailingAnchor),
                             webView.topAnchor.constraint(equalTo: webViewBase.topAnchor),
                             webView.bottomAnchor.constraint(equalTo: webViewBase.bottomAnchor)])
```

**注意**
1つ注意したいのが、`webView.translatesAutoresizingMaskIntoConstraints = false`を行わないとWKWebViewで制約決める時に他の制約とぶつかり`Unable to simultaneously satisfy constraints.`がログに出る。

これは`AutoresizingMask`が設定されていると自動的にAutoLayoutの制約に置き換えてくれるのだが、defaultで`translatesAutoresizingMaskIntoConstraints`は`true`になっているため、`false`を設定してやらないといけない。