WKWebViewをさわってみた

# はじめに
これからwebViewを使う機会があるのですが、私はWKWebViewについて知りませんでした。
ドキュメントを見ても処理についてわからないため触ってみてWKWebViewについて学びました。

# 環境
- Mac OS Monterey 12.6
- Xcode version 14.2
- Swift version 5.7.2

# 前提
- WKWebViewとは

https://developer.apple.com/documentation/webkit/wkwebview

WebKitフレームワークのクラスの一つ
アプリ内でWebページを表示、操作したりする場合などに使われているクラスです。

- WKNavigationDelegateとは

https://developer.apple.com/documentation/webkit/wknavigationdelegate

WKWebViewのナビゲーションリクエストの進行状況を追跡するためのプロトコルです。
これを通じてライフサイクルを理解することができそうなので、今回調査してみることにしました。

# やってみたこと
webView実装時には特定のページから遷移してwebView表示を行うため、
ボタンタップでwebView表示する簡単なサンプルを作成しました。
![Simulator Screen Recording - iPhone 12 Pro Max - 2023-09-11 at 04.22.49.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618527/7017304a-471b-83d0-efbb-d29d6c44153e.gif)

## webViewライフサイクルの動きを確認します
今回はWKWebViewのライフサイクルを確認できるように
メソッド内にメソッド名をprint出力させるように作りました。

```swift:SecondViewController.swift
import UIKit
import WebKit

class SecondViewController: UIViewController {
    
    var webView: WKWebView!
    
    override func loadView() {
        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.navigationDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "SecondViewController"
        let myURL = URL(string: "https://www.apple.com")
        let myRequest = URLRequest(url: myURL!)
        webView.load(myRequest)
    }
}

extension SecondViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        print("decidePolicyFor navigationAction: WKNavigationAction")
        decisionHandler(.allow)
    }
    
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        print("didStartProvisionalNavigation navigation: WKNavigation!")
    }
    
    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationResponse: WKNavigationResponse,
                 decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
        print("decidePolicyFor navigationResponse: WKNavigationResponse")
        decisionHandler(.allow)
    }
    
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
        print("didCommit navigation: WKNavigation!")
    }
    
    func webView(_ webView: WKWebView,
                 didReceive challenge: URLAuthenticationChallenge,
                 completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        print("didReceive challenge: URLAuthenticationChallenge")
        completionHandler(.useCredential, nil)
    }
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print("didFinish navigation: WKNavigation!")
    }
    
    func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError: Error) {
        print("didFailProvisionalNavigation navigation: WKNavigation!")
    }
    
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError: Error) {
        print("didFail navigation: WKNavigation!")
    }
    
    func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation: WKNavigation!) {
        print("didReceiveServerRedirectForProvisionalNavigation: WKNavigation!")
    }
}
```
# 結果
ビルドしてデバッグログを確認します。
`https://www.apple.com`を表示した場合の出力結果です。
メソッド名が長いので第二引数のラベルを表示しています。

ボタンタップ後のWKNavigationDelegate log
```swift
1. decidePolicyFor navigationAction: WKNavigationAction
2. didReceive challenge: URLAuthenticationChallenge
3. didStartProvisionalNavigation navigation: WKNavigation!
4. decidePolicyFor navigationResponse: WKNavigationResponse
5. didCommit navigation: WKNavigation!
6. decidePolicyFor navigationAction: WKNavigationAction
7. didReceive challenge: URLAuthenticationChallenge
8. didFinish navigation: WKNavigation!
```

何回か試行して`2.`と`3.`の順番が前後することがありました。
また別サイトを表示したときに下記のメソッドが連続で呼ばれる場合がありました。

```swift
webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
```
各メソッドがどんな用途で、どのタイミングで呼び出されるのかを調べてみます。

# WKNavigationDelegateメソッドについて調査

どんな用途で使われるか簡単にまとめました。

### ナビゲーションリクエストの許可または拒否

**decidePolicyFor navigationAction**
タイミング：リクエスト前

指定された設定やアクション（タップなど）に基づいて、新しいコンテンツに移動する許可または拒否を応答します。
WebViewは、インタラクションが発生した後ページをロードしようとする前にこのメソッドを呼び出します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455641-webview


**decidePolicyFor navigationResponse**
タイミング：レスポンス後

webViewが元のURLリクエストに対する応答を受信した後、ナビゲーションリクエストを許可または拒否するには、このメソッドを使用します。
パラメーターには、レスポンスに含まれるデータのタイプなどの詳細が含まれます。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview

### リクエストのロード進行状況の追跡

**didStartProvisionalNavigation**
タイミング：ページ読み込み開始前

ナビゲーションリクエストを処理するための暫定的な承認を受け取った後、
そのリクエストに対するレスポンスを受け取る前にこのメソッドを呼び出します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455621-webview

**didReceiveServerRedirectForProvisionalNavigation**
タイミング：リダイレクト時

WebViewが要求のサーバーリダイレクトを受信したときに呼び出します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455627-webview

**didCommit**
タイミング：ページ読み込み開始時

WebViewがメインフレームのコンテンツの受信を開始したときに呼ばれます。
webView(_:decidePolicyFor:decisionHandler:)がナビゲーションのレスポンスを承認した後、WebViewは処理を開始、変更の準備が整うと、WebViewはメインフレームを更新し始める前に、このメソッドを呼び出します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455635-webview

**didFinish**
タイミング：ページ読み込み完了時

ナビゲーションが完了した時に呼ばれます。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455629-webview


### 認証対応

**didReceive challenge**
タイミング：認証が必要な時

認証チャレンジ（HTTPSのサーバ認証やHTTP Basic認証、Digest認証など）に応答する時に呼ばれます。
このメソッドを実装しない場合、WebViewは`URLSession.AuthChallengeDisposition.rejectProtectionSpace`で認証チャレンジに応答します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview


**authenticationChallenge challenge**
タイミング：非推奨のTLSプロトコル接続時

非推奨バージョンのTLS(Transport Layer Security)を使用する接続を続行するかどうか
これを実装しない場合、システム設定を使用して非推奨バージョンのTLSの使用を許可するかどうかを決定します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/3601237-webview

### エラー対応

**didFailProvisionalNavigation**
ページ読み込み開始時でのエラー発生時（通信圏外など）に呼び出します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455637-webview

**didFail**
ページ読み込み途中でのエラー発生時（HTML読み込み中のキャンセルなど）に呼び出します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455623-webview

**webViewWebContentProcessDidTerminate**
ページ読み込みが何らかの理由で終了した時に呼び出します。

Web ビューは、Web コンテンツのレンダリングと管理に別のプロセスを使用します。
WebKitは、指定されたWebViewのプロセスが何らかの理由で終了したときにこのメソッドを呼び出します。

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455639-webviewwebcontentprocessdidtermi


# 考察

調査した結果、各メソッドの用途に関してはおおよそ把握できました。　
しかしタイミングに関しては、結果でも触れた
> 何回か試行して2.と3.の順番が前後することがありました。
また別サイトを表示したときに下記のメソッドが連続で呼ばれる場合がありました。
```swift
webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
```
上記の部分が解決できていないので、深掘りしてみようと思います。
影響範囲を調べるため、いくつかのサイトを表示させた際のログを比較してみました。

## webサイト毎の違いを比較する

表示するwebサイト毎の違いを比較してみます。
リクエスト前のメソッド内にリクエストページのURLを表示するようにしました。
```swift
    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        print("リクエスト前: decidePolicyFor navigationAction: WKNavigationAction")
        
        let url = navigationAction.request.url
        print("リクエストページのURLを取得: ", url ?? "")
...省略
```
Apple、Google、Qiitaを表示してデバッグログを確認します。

## 結果

デバッグログ出力結果
`https://www.apple.com`を表示した場合
```swift
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  https://www.apple.com/
ページ読み込み開始前: didStartProvisionalNavigation navigation: WKNavigation!
レスポンス後: decidePolicyFor navigationResponse: WKNavigationResponse
ページの読み込み開始: didCommit navigation: WKNavigation!
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
読み込み完了: didFinish navigation: WKNavigation!
```
`https://www.apple.com`を表示した場合の出力結果をシーケンス図にまとめました。
```mermaid
sequenceDiagram
    autonumber
    actor user as ユーザー
    participant wknv as WKNavigationDelegate
    Note over user: ボタンタップ
    user ->>  wknv: 
    Note over wknv: 1. 認証チャレンジ<br>didReceive challenge
    Note over wknv: 2. リクエスト前 <br> decidePolicyFor navigationAction
    Note over wknv: 3. ページ読み込み開始前<br>didStartProvisionalNavigation
    Note over wknv: 4. レスポンス後<br>decidePolicyFor navigationResponse
    Note over wknv: 5. ページ読み込み開始<br>didCommit
    Note over wknv: 6. 認証チャレンジ<br>didReceive challenge
    Note over wknv: 7. ページ読み込み完了<br>didFinish
    wknv ->> user: 
    Note over user: webView表示
```

`https://www.google.com/`を表示した場合
```swift
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  https://www.google.com/
ページ読み込み開始前: didStartProvisionalNavigation navigation: WKNavigation!
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
レスポンス後: decidePolicyFor navigationResponse: WKNavigationResponse
ページの読み込み開始: didCommit navigation: WKNavigation!
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
読み込み完了: didFinish navigation: WKNavigation!
```
`https://qiita.com/`を表示した場合
```swift
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  https://qiita.com/
ページ読み込み開始前: didStartProvisionalNavigation navigation: WKNavigation!
レスポンス後: decidePolicyFor navigationResponse: WKNavigationResponse
ページの読み込み開始: didCommit navigation: WKNavigation!
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  https://236a902c5d2e2499323d3079471c4a90.safeframe.googlesyndication.com/safeframe/1-0-40/html/container.html
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
レスポンス後: decidePolicyFor navigationResponse: WKNavigationResponse
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  https://tpc.googlesyndication.com/sodar/sodar2/225/runner.html
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  https://www.google.com/recaptcha/api2/aframe
レスポンス後: decidePolicyFor navigationResponse: WKNavigationResponse
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  about:blank
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
レスポンス後: decidePolicyFor navigationResponse: WKNavigationResponse
読み込み完了: didFinish navigation: WKNavigation!
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  about:blank
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  https://236a902c5d2e2499323d3079471c4a90.safeframe.googlesyndication.com/safeframe/1-0-40/html/container.html
レスポンス後: decidePolicyFor navigationResponse: WKNavigationResponse
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  https://tpc.googlesyndication.com/sodar/Enqz_20U.html
レスポンス後: decidePolicyFor navigationResponse: WKNavigationResponse
リクエスト前: decidePolicyFor navigationAction: WKNavigationAction
リクエストページのURLを取得:  about:blank
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
認証チャレンジ: didReceive challenge: URLAuthenticationChallenge
```
Apple、GoogleのWebページを表示したときはリクエスト1回に対して
Qiitaを表示したときは複数のURLリクエスト、認証チャレンジなどが走っていることを確認しました。
リクエスト時のURL`container.html`や`runner.html`から、CSSやテストランナーなどサブリソースが関係している可能性があります。



## 現状と今後の取り組み

WebView表示時に`WKWebView`のいくつかのデリゲートメソッドが複数回呼び出されている現象を確認しました。
これはWebページ側の実装や読み込むリソース、フレーム読み込みといった動作に起因しているのでないかと予想しています。

特定のサイトでこの現象が起きていることから、そのサイトの構造や使用している技術を深く調査することで、原因の手がかりを得ることができるかもしれません。例えば開発者ツールを利用してリソースを調査する方法が考えられます。

# 今回学んだこと

- WKNavigationDelegateの各メソッドの用途
- webサイト毎にデリゲートメソッドの呼ばれるタイミングは異なること

# 感想

Delegateメソッドの役割が把握できたので実装する際にどのメソッドに処理を書くかが明確になりました。
また、ライフサイクルの調査方法は他のケースでも使えそうなので活用したいです。
`WKNavigationDelegate`の他に`WKUIDelegate`も調査してみたいと思います。
今回デリゲートメソッドのタイミングに関する疑問への答えは掴めていませんが、今後は開発者ツールを利用して調査を進めることで、原因の手がかりを見つけることができると期待しています。


# 参考

https://note.com/npaka/n/n3acc1e1b8036#HawaA

https://qiita.com/Soccerboy_Hamada/items/c87d655f2a35cb065c90

https://qiita.com/1234224576/items/0ecee5ce877e99ac3647