[iOS] [Swift] WebViewコンテンツを取得可能か事前チェックしてOKの場合のみWebView画面に遷移したい場合の実装例

# TL;DR

表題のような要求を実現するために、
**URLSessionでHTML文字列を取得し `WKWebView.loadHTMLString(_:baseURL:)` によってロードする、**
という内容です。

特に目新しくもなく高等テクニックでもないのですが、案外気づいていない人も多いような気がして、記事にしてみました。

# 実現したいこと

（業務要件として）親ViewController側でサーバーレスポンスを事前チェックし、

- 正常の場合、子のWebView画面に遷移してコンテンツを表示
- エラーの場合、親ViewControllerに止まり何らかのエラー処理（例えばAlertを表示）

**正常時（WebView画面に遷移してコンテンツを表示）**
<img width="834" alt="normal.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122917/56f75ceb-9dd8-c1ea-ca26-fb298f0714ba.png">

**エラー時（遷移せずにAlert表示）**
<img width="798" alt="error.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122917/171839f2-7d62-f970-1d3b-ffb9fa87fcbc.png">


# 実現手法

## 環境

- Xcode 11.0
- Swift 5.1

## Storyboard

<img width="1392" alt="storyboard.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122917/48a3d65b-0c97-34ff-1be4-4e9ce2079e5e.png">

## コード

画面間のパラメータとしてHTML文字列とURLが必要になります。
**`(String, URL)`のタプルに`typealias`によって型の別名を与えておくとリーダブルになるかと思います。**
（以下のコード例では`WebViewSource`という`typealias`を付けています）

```swift:FirstViewController.swift
import UIKit

class FirstViewController: UIViewController {

    // Requestボタンtap
    @IBAction func request(_ sender: Any) {
// Good Request
        let urlOK = "https://qiita.com/y-some/items/f6bf2106597cde9f1969"
        guard let url = URL(string: urlOK) else {
            return
        }
// Bad Request(404)
//        let urlNG = "https://qiita.com/y-some/items/XXXXXXXXXXXXXXXXXXXX"
//        guard let url = URL(string: urlNG) else {
//            return
//        }
        
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            // エラーハンドリング
            if let error = error {
                self.popErrorAlert(with: error.localizedDescription)
                return
            }
            guard let response = response as? HTTPURLResponse else {
                self.popErrorAlert(with: "bad response")
                return
            }
            if response.statusCode != 200 {
                self.popErrorAlert(with: "statusCode: \(response.statusCode)")
                return
            }
            guard let data = data, let url = response.url else {
                self.popErrorAlert(with: "bad data")
                return
            }
            // 正常系：2画面目（WebView）に遷移する。senderとしてHTML文字列とbaseURLを渡す
            let htmlString = String(data: data, encoding: .utf8) ?? ""
            let webViewSource = WebViewSource(htmlString: htmlString, baseURL: url)
            DispatchQueue.main.async {
                self.performSegue(withIdentifier: "showWebView", sender: webViewSource)
            }
        }.resume()
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // 2画面目（WebView）にパラメータとしてHTML文字列とbaseURLを渡す
        guard
            let webViewController = segue.destination as? WebViewController,
            let webViewSource = sender as? WebViewSource else {
                return
        }
        webViewController.webViewSource = webViewSource
    }
    
    func popErrorAlert(with message: String) {
        DispatchQueue.main.async {
            let alertController = UIAlertController(title:"Error", message: message, preferredStyle: .alert)
            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            self.present(alertController, animated: true, completion: nil)
        }
    }
    
}
```

```swift:WebViewController.swift
import UIKit
import WebKit

typealias WebViewSource = (htmlString: String, baseURL: URL)

class WebViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    var webViewSource: WebViewSource?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        guard let webViewSource = webViewSource else {
            preconditionFailure("webViewSource is nil")
        }
        // メインコンテンツはパラメータにて受け取ったHTML文字列、CSSやJSなどのリソースはbaseURLによってロード
        webView.loadHTMLString(webViewSource.htmlString, baseURL: webViewSource.baseURL)
    }
    
}
```

# 補足

- `baseURL`の設定を忘れるとCSSやJavaScriptがロードされません。
- `baseURL`はリダイレクト後のURLを設定する必要がありますので、HTTPURLResponse.urlを使用します。