📚

WKWebViewについてのまとめ(SwiftUIでの使い方も)

2021/10/03に公開

WKWebViewのサンプルを作成したのでメモを残しておきます。

ソースコードはこちら

2019/11/16: WKWebView内の画面遷移のフックやjsとの連携について加筆・修正しました
2021/01/02: SwiftUIでWKWebViewを使う方法を追記しました

開発環境

  • Xcode:11.2.1
  • Swift:5.1.2

実装

WKWebViewのセットアップ

Setup
// MARK: - Setup WKWebView
private func setupWKWebView() {
        
    let webConfig = WKWebViewConfiguration()
    wkWebView = WKWebView(frame: .zero, configuration: webConfig)
    wkWebView.navigationDelegate = self  // Delegate①:画面の読み込み・遷移系
    wkWebView.uiDelegate = self  // Delegate②:jsとの連携系
        
    view = wkWebView
}

Webページのロード

Load
// MARK: - Load Web Page
private func load(withURL urlStr:String) {
    guard let url = URL(string: urlStr) else { return }
    let request = URLRequest(url: url)
        
    wkWebView.load(request)
}

Delegateメソッド

Delegate①:画面の読み込み・遷移系

WKNavigationDelegate
// MARK: - 読み込み設定(リクエスト前)
func webView(_ webView: WKWebView,
             decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    print("リクエスト前")
        
    /*
     * WebView内の特定のリンクをタップした時の処理などが書ける(2019/11/16追記)
     */
    let url = navigationAction.request.url
    print("読み込もうとしているページのURLが取得できる: ", url ?? "")
    // リンクをタップしてページを読み込む前に呼ばれるので、例えば、urlをチェックして
    // ①AppStoreのリンクだったらストアに飛ばす
    // ②Deeplinkだったらアプリに戻る
    // みたいなことができる

    /*  これを設定しないとアプリがクラッシュする
     *  .allow  : 読み込み許可
     *  .cancel : 読み込みキャンセル
     */
    decisionHandler(.allow)
}
    
// MARK: - 読み込み準備開始
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
    print("読み込み準備開始")
}
    
// MARK: - 読み込み設定(レスポンス取得後)
func webView(_ webView: WKWebView,
             decidePolicyFor navigationResponse: WKNavigationResponse,
             decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
    print("レスポンス取得後")
        
    /*  これを設定しないとアプリがクラッシュする
     *  .allow  : 読み込み許可
     *  .cancel : 読み込みキャンセル
     */
    decisionHandler(.allow)
    // 注意:受け取るレスポンスはページを読み込みタイミングのみで、Webページでの操作後の値などは受け取れない
}
    
// MARK: - 読み込み開始
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
    print("読み込み開始")
}
    
// MARK: - ユーザ認証(このメソッドを呼ばないと認証してくれない)
func webView(_ webView: WKWebView,
             didReceive challenge: URLAuthenticationChallenge,
             completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    print("ユーザ認証")
    completionHandler(.useCredential, nil)
}
    
// MARK: - 読み込み完了
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    print("読み込み完了")
}
    
// MARK: - 読み込み失敗検知
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError: Error) {
    print("読み込み失敗検知")
}
    
// MARK: - 読み込み失敗
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError: Error) {
    print("読み込み失敗")
}
    
// MARK: - リダイレクト
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation:WKNavigation!) {
    print("リダイレクト")
}

Delegate②:jsとの連携系(2019/11/16追記)

WKUIDelegate
// alertを表示する
func webView(_ webView: WKWebView,
             runJavaScriptAlertPanelWithMessage message: String,
             initiatedByFrame frame: WKFrameInfo,
             completionHandler: @escaping () -> Void) {
    let alertController = UIAlertController(title: "title",
                                            message: "message",
                                            preferredStyle: .alert)
        
    let okAction = UIAlertAction(title: "OK", style: .default) { action in
        completionHandler()
    }
        
    alertController.addAction(okAction)
        
    present(alertController ,animated: true ,completion: nil)
}

// confirm dialogを表示する
func webView(_ webView: WKWebView,
             runJavaScriptConfirmPanelWithMessage message: String,
             initiatedByFrame frame: WKFrameInfo,
             completionHandler: @escaping (Bool) -> Void) {
    let alertController = UIAlertController(title: "title",
                                            message: "message",
                                            preferredStyle: .alert)
        
    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { action in
        completionHandler(false)
    }
    let okAction = UIAlertAction(title: "OK", style: .default) { action in
        completionHandler(true)
    }
        
    alertController.addAction(cancelAction)
    alertController.addAction(okAction)
        
    present(alertController ,animated: true ,completion: nil)
}

// 入力フォーム(prompt)を表示する
func webView(_ webView: WKWebView,
             runJavaScriptTextInputPanelWithPrompt prompt: String,
             defaultText: String?,
             initiatedByFrame frame: WKFrameInfo,
             completionHandler: @escaping (String?) -> Void) {
    let alertController = UIAlertController(title: "title",
                                            message: prompt,
                                            preferredStyle: .alert)
        
    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { action in
        completionHandler("")
    }
        
    let okHandler = { () -> Void in
        if let textField = alertController.textFields?.first {
            completionHandler(textField.text)
        } else {
            completionHandler("")
        }
    }
    let okAction = UIAlertAction(title: "OK", style: .default) { action in
        okHandler()
    }
        
    alertController.addTextField() { $0.text = defaultText }
    alertController.addAction(cancelAction)
    alertController.addAction(okAction)
        
    present(alertController ,animated: true ,completion: nil)
}

イベントを検知して、アラートやダイアログはネイティブ側で作って表示するイメージ。

他にJsと連携してできること(2019/11/16追記)

①js側から処理を受け取ってネイティブ側の処理を実行する
②ネイティブ側からjsのfunctionを叩く・DOMをいじる
これに関しては次回更新の時に記載できれば良いなと思います。

#実行結果(画面の読み込み・遷移系)

実行結果
リクエスト前
読み込み準備開始
レスポンス取得後
読み込み開始
ユーザ認証
ユーザ認証
読み込み完了

想定通りに動いてくれて良かった(認証が2回通ってるけど...)

SwiftUIでWKWebViewを使う

結論から言うと、現状では UIViewRepresentable を継承して自作するしかなさそうです。(2020/01/02時点)

UIViewRepresentableとは
SwiftUIにてUIKitのViewを使用するためのラッパーです。
UIKitのViewをSwiftUIで使用するにはUIViewRepresentableを使用する必要があります。

MyWebView

import SwiftUI
import WebKit

struct MyWebView: UIViewRepresentable {
    
    let url: String
    private let observable = WebViewURLObservable()
    
    /// 監視する対象を指定して値の変化を検知する
    var observer: NSKeyValueObservation? {
        observable.instance
    }
    
    // MARK: - UIViewRepresentable
    /// 表示するViewのインスタンスを生成
    /// SwiftUIで使用するUIKitのViewを返す
    func makeUIView(context: Context) -> WKWebView {
        WKWebView()
    }
    
    // MARK: - UIViewRepresentable
    /// アプリの状態が更新される場合に呼ばる
    /// Viewの更新処理はこのメソッドに記述する
    func updateUIView(_ uiView: WKWebView, context: Context) {

        /// WKWebViewのURLが変わったこと(WebView内画面遷移)を検知して、URLをログ出力する
        observable.instance = uiView.observe(\WKWebView.url, options: .new) { view, change in
            if let url = view.url {
                print("Page URL: \(url)")
            }
        }
        
        /// URLを指定してWebページを読み込み
        uiView.load(URLRequest(url: URL(string: url)!))
    }
}

// MARK:  WKWebViewのURLが変わったこと(WebView内画面遷移)を検知するための `ObservableObject`
private class WebViewURLObservable: ObservableObject {
    @Published var instance: NSKeyValueObservation?
}

使い方

ContentView

import SwiftUI

struct ContentView: View {
    var body: some View {
        MyWebView(url: "https://www.apple.com/")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

まとめ

まだまだ深く突っ込めると思うのでもう少し触ってみようと思いますー
ご指摘などいただけると嬉しいです。

Discussion