【Swift4】Basic認証がかかったページを開ける WKWebView を作成してみた

## はじめに
Webページを表示するとき、これまで `SFSafariViewController` を使っていましたが、 `WKWebView` を使ってカスタマイズしてみました。
まずは、簡単なWKWebViewを作成して、Basic認証ページを開くまでやってみます。
今回はStoryboardは使用せず、すべてコードでAutoLayoutの制約を付けています。

## やってみる

### 1. WKWebViewの宣言

WKWebViewのインスタンスを作成する。

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

class WebViewController: UIViewController {
    private lazy var webView: WKWebView = {
        let webView = WKWebView()
        webView.translatesAutoresizingMaskIntoConstraints = false
        return webView
    }()
}
```

### 2. レイアウトの設定

作成したWKWebViewをセットし、画面いっぱいに表示するようにする。
SafeAreaの考慮もしておく。

```swift:WebViewController.swift
override func loadView() {
    view = UIView()
    view.backgroundColor = UIColor.white
}

override func viewDidLoad() {
    super.viewDidLoad()

    view.addSubview(webView)

    NSLayoutConstraint.activate([
        webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0.0),
        webView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
        webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
        webView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0)
    ])
}
```

### 3. Webページを表示

`load` メソッドでWebページを表示する。

```swift:WebViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()

    let url = URL(string: "https://google.co.jp")
    let request = URLRequest(url: url!)        
    webView.load(request)

    view.addSubview(webView)
    // ...
}
```

これでWebページを表示するところまでできました。
この状態で、Basic認証がかかったWebページにアクセスすると、 Authorization Required のエラー画面が表示されてしまいます。

### 4. Basic認証がかかったページを開けるようにする

WKWebViewに `WKNavigationDelegate` を指定。

```swift:WebViewController.swift
private lazy var webView: WKWebView = {
    let webView = WKWebView()
    webView.translatesAutoresizingMaskIntoConstraints = false
    webView.navigationDelegate = self
    return webView
}()
```

`WKNavigationDelegate` のプロトコルが必要なので、ViewControllerに指定。
個人的な好みで extensionとして切り分けていますが、ViewControllerの中に入れてしまっても問題ありません。

```swift:WebViewController.swift
extension WebViewController: WKNavigationDelegate {
    /// 認証に応答する必要があるときに呼ばれる
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge,
                 completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let credential = URLCredential(user: "user", password: "password", persistence: URLCredential.Persistence.forSession)
        completionHandler(.useCredential, credential)
    }
}
```

これでBasic認証がかかったWebページを表示するところまでできました。

### 5. Basic認証のユーザ名、パスワードをユーザに入力させる

`UIAlertController` を使って、ユーザー名とパスワードの入力を求めるアラートを出す。

```swift:WebViewController.swift
extension WebViewController: WKNavigationDelegate {
    /// 認証に応答する必要があるときに呼ばれる
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge,
                 completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        
        let alert = UIAlertController(title: "Basic認証", message: "ユーザ名とパスワードを入力してください。", preferredStyle: .alert)
        
        // ユーザ名のテキストフィールドを追加
        alert.addTextField(configurationHandler: {(textField: UITextField!) -> Void in
            textField.placeholder = "ユーザ名"
            textField.tag = 1
        })
        
        // パスワードのテキストフィールドを追加
        alert.addTextField(configurationHandler: {(textField: UITextField!) -> Void in
            textField.placeholder = "パスワード"
            textField.isSecureTextEntry = true
            textField.tag = 2
        })
        
        // ログインボタンの設定
        let okAction = UIAlertAction(title: "ログイン", style: .default, handler: {
            (action:UIAlertAction!) -> Void in

            var user = ""
            var password = ""

            if let textFields = alert.textFields {
                for textField in textFields {
                    if textField.tag == 1 {
                        user = textField.text ?? ""
                    } else if textField.tag == 2 {
                        password = textField.text ?? ""
                    }
                }
            }

            let credential = URLCredential(user: user, password: password, persistence: URLCredential.Persistence.forSession)
            completionHandler(.useCredential, credential)
        })
        alert.addAction(okAction)

        // キャンセルボタンの設定
        let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler: {
            (action:UIAlertAction!) -> Void in
            completionHandler(.cancelAuthenticationChallenge, nil)
        })
        alert.addAction(cancelAction)

        // アラートを画面に表示
        present(alert, animated: true, completion: nil)
    }
}
```

注意点としては、 `completionHandler` は必ず呼び出さないといけません。
ですので、キャンセルボタンを押したときは、 `.cancelAuthenticationChallenge` を使って、明示的にキャンセルします。
もし `completionHandler` を呼び出さなかった場合、実行エラーとなります。

ユーザ名、パスワードのUITextFieldは、もっと凝るべきでしょうが今回のテーマから離れるので、最低限しか書いていません。

## ソースコード

ここまで書いたコードをまとめて載せておきます。

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

class WebViewController: UIViewController {
    // MARK: - View

    private lazy var webView: WKWebView = {
        let webView = WKWebView()
        webView.translatesAutoresizingMaskIntoConstraints = false
        webView.navigationDelegate = self
        return webView
    }()

    // MARK: - ViewController lifecycle

    override func loadView() {
        view = UIView()
        view.backgroundColor = UIColor.white
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Webページを表示
        let url = URL(string: "https://google.co.jp")
        let request = URLRequest(url: url!)        
        webView.load(request)

        view.addSubview(webView)

        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0.0),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
            webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0)
        ])
    }
}

extension WebViewController: WKNavigationDelegate {
    /// 認証に応答する必要があるときに呼ばれる
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge,
                 completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        let alert = UIAlertController(title: "Basic認証", message: "ユーザ名とパスワードを入力してください。", preferredStyle: .alert)

        // ユーザ名のテキストフィールドを追加
        alert.addTextField(configurationHandler: {(textField: UITextField!) -> Void in
            textField.placeholder = "ユーザ名"
            textField.tag = 1
        })

        // パスワードのテキストフィールドを追加
        alert.addTextField(configurationHandler: {(textField: UITextField!) -> Void in
            textField.placeholder = "パスワード"
            textField.isSecureTextEntry = true
            textField.tag = 2
        })

        // ログインボタンの設定
        let okAction = UIAlertAction(title: "ログイン", style: .default, handler: {
            (action:UIAlertAction!) -> Void in

            var user = ""
            var password = ""

            if let textFields = alert.textFields {
                for textField in textFields {
                    if textField.tag == 1 {
                        user = textField.text ?? ""
                    } else if textField.tag == 2 {
                        password = textField.text ?? ""
                    }
                }
            }

            let credential = URLCredential(user: user, password: password, persistence: URLCredential.Persistence.forSession)
            completionHandler(.useCredential, credential)
        })
        alert.addAction(okAction)

        // キャンセルボタンの設定
        let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler: {
            (action:UIAlertAction!) -> Void in
            completionHandler(.cancelAuthenticationChallenge, nil)
        })
        alert.addAction(cancelAction)

        // アラートを画面に表示
        present(alert, animated: true, completion: nil)
    }
}
```