iOS
Swift

WKWebViewに関する調査メモ

More than 3 years have passed since last update.

WKWebViewを用いた時に調査した技術メモ。2015年3月上旬時点のもの。
かなり雑です。。

WKWebViewの特徴

プログレスの取得

estimatedProgressプロパティにより取得可

タイトル・URLの取得

title,URLプロパティが追加された

safariのような閲覧履歴の取得

閲覧履歴が管理される

エッジスワイプで戻る、進む

allowsBackForwardNavigationGesturesプロパティをtrueにすることで設定可

プロパティのKVO対応

title,URL,loading,estimatedProgress,hasOnlySecureContent,canGoBack,canGoFowardプロパティがKVO対応

Javascriptとの連携

以下に詳細を記述

JavaScriptとの連携

evaluateJavaScript(_ javaScriptString: String, completionHandler completionHandler: ((AnyObject!, NSError!) -> Void)?)
によりJavaScriptのコードを実行可能(UIWebViewのstringByEvaluationJavascript...に相当)
この機能に加えて、UsersScriptが追加された。

UsersScript

(概要)

WKWebViewには3つの主要なクラスが存在する。
・WKWebView
・WKWebViewConfiguration
・WKUserContentController
まず、WKWebViewのコンストラクタからWKWebViewConfigurationクラスのインスタンスを与える事が出来る。
これにより幾つかの設定を増やす事が出来る。(classReference:https://developer.apple.com/library/ios/documentation/WebKit/Reference/WKWebViewConfiguration_Ref/index.html#//apple_ref/occ/instp/WKWebViewConfiguration/mediaPlaybackRequiresUserAction
そして、WKWebViewConfigurationクラスの大切な点はuserContentControllerプロパティである。
このプロパティにWKUserContentControllerクラス(classReference:https://developer.apple.com/library/ios/documentation/WebKit/Reference/WKUserContentController_Ref )のインスタンスを登録する。
WKUserContentControllerのインスタンスはaddScriptMessageHandlerをコールし、自信で設定したjavascriptを呼び出す事ができる。(要WKScriptMessageHandlerProtocol)
ただし、これはWKWebViewがロードされる前にセットアップする必要がある。

(実装方法)

WKUsersScriptクラス(https://developer.apple.com/library/ios/documentation/WebKit/Reference/WKUserScript_Ref/#//apple_ref/occ/instm/WKUserScript/initWithSource:injectionTime:forMainFrameOnly
このクラスを用いる事によりDOMのロード前・後にJavaScriptを注入することが可能。
init(source source: String, injectionTime injectionTime: WKUserScriptInjectionTime, forMainFrameOnly forMainFrameOnly: Bool)
※injectionTime: WKUserScriptInjectionTimeAtDocumentStart / WKUserScriptInjectionTimeAtDocumentEnd)

上記のコードによりWKUserScriptクラスのインスタンスを作成する。
sourceの部分にはStringでjavascriptコードを渡す。例えば以下のような文字列を渡すことでjavascriptからNaitiveにメッセージを送信することが出来る。
window.webkit.messageHandlers.{MessageHandlerName}.postMessage({javascriptCode})

このインスタンスをWKUserContentControllerのインスタンスのaddUserScriptメソッドにより設定。更にMessageHandlerも登録する必要がある。
・UserScriptを追加
addUserScript(_ userScript: WKUserScript)

・MessageHandleの登録
addScriptMessageHandler(_ scriptMessageHandler: WKScriptMessageHandler, name name: String)

更にWKWebViewConfigurationクラスのインスタンスのuserContentControllerプロパティにWKUserContentControllerのインスタンスを設定してやることで実行されるようになる。
javascriptが実行された際には以下のWKScriptMessageHandlerProtocolのメソッドが呼ばれる。

userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage)

これら一連の処理はWKWebViewのインスタンスの初期化前に行う必要がある。

WKNavigationDelegate

このプロトコルはWebページの画面遷移を監視し、フックする事ができる。

webView(_:didCommitNavigation:) //遷移開始時
webView(_:didFailNavigation:withError:) //遷移中にエラーが発生した時
webView(_:didFailProvisionalNavigation:withError:) //ページ読み込み時にエラーが発生した時
webView(_:didFinishNavigation:) //ページ読み込みが完了した時
webView(_:didReceiveAuthenticationChallenge:completionHandler:) //認証が必要な時
webView(_:didReceiveServerRedirectForProvisionalNavigation:) //リダイレクトされた時
webView(_:didStartProvisionalNavigation:) //ページ読み込みが開始された時

更にこのプロトコルには画面遷移を許可するかどうかを決定できるメソッドが定義されている。

webView(_:decidePolicyForNavigationAction:decisionHandler:) //Webサイトにリクエストを送る前に判断
webView(_:decidePolicyForNavigationResponse:decisionHandler:) //Webサイトからレスポンスが帰ってきた後に判断

この2つのメソッドは呼ばれるタイミングが異なる。最初のメソッドはWebサイトへリクエストを送る前に遷移するかどうかを決定する。
このメソッドは遷移時、didStartProvisionalNavigationメソッドより前にまず1度呼ばれる。
それに対して、次のメソッドはWebサイトから応答が帰ってきた後に遷移するかどうかを決定することができる。つまりMIMEtypeをみて遷移するかどうかを決定する、などといったことができる。
この2つのメソッドは複数回コールされる事があるので注意。
decidePolicyForNavigationAction:decisionHandlerはtext/htmlリクエストを送る度に実行される。ただしステータスコード301,302(他もあるかも)が帰ってくるアドレスに対してはURLにabout:blankが設定されている。
decidePolicyForNavigationResponse:decisionHandlerはtargetFrameのURLがabout:blank以外であった場合のみ、読み込み完了時に呼ばれる。
(どちらも独自で調査した結果なので間違ってるかもしれない)

遷移を許可するかどうかはdecisionHandlerの引数で渡されたクロージャを呼び出して決定する。

enum WKNavigationActionPolicy : Int {
    case Cancel
    case Allow
}
enum WKNavigationResponsePolicy : Int {
    case Cancel
    case Allow
}

例)decisionHandler(.Cancel)

遷移先の情報はWKNavigationAction、WKNavigationResponseクラスのインスタンスが保持している。

WKNavigationAction

このクラスのインスタンスは以下のものを保持している。

request

遷移先に関してのNSURLRequestオブジェクト

sourceFrame

遷移元に関してのWKFrameInfoオブジェクト

targetFrame

遷移先に関してのWKFrameInfoオブジェクト

navigationType

遷移の種類を示すenum値

enum WKNavigationType : Int {
    case LinkActivated //aタグによる遷移
    case FormSubmitted //フォームの送信による遷移
    case BackForward //進む、戻るによる遷移
    case Reload //更新による遷移
    case FormResubmitted //フォームの再送信による遷移
    case Other //その他の方法による遷移
}

WKNavigationResponse

canShowMIMEType

遷移先のMIMETypeが表示可能かどうかを示すbool値

forMainFrame

遷移がmainFrameで行われるかを示すbool値?

response

遷移先に関してのNSURLResponseオブジェクト

WKFrameInfo Class

このクラスに関しては現状詳しい情報はなく、確かな記述ではない。
所持するプロパティは以下の通り。

mainFrame

そのフレームがメインフレームであるかを示すBool値

request

そのフレームが所持するNSURLRequestオブジェクト

このクラスは各遷移先、元などのページの情報を一時的に保存しておくクラスで,以下のメソッドの引数であるWKNavigationActionオブジェクトの
targetFrameとsourceFrameプロパティから取得できる。

webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) 
webView(webView: WKWebView, createWebViewWithConfiguration configuration: WKWebViewConfiguration, forNavigationAction navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView?

mainFrameというプロパティが何を意味するのかについて。

text/htmlタイプのHTTPリクエストが走ったときdecidePolicyForNavigationAction navigationActionがコールされるとし、
そのリクエストがWebページのmainとなるもの(恐らく最初にリクエストされるもの、s.amebaであればs.ameba.comというtext/html)であれば
trueを。そのページを構成している要素を読み込んでいる時はfalseを返すものだと考えられる。

targetFrameとは、decidePolicyForNavigationAction navigationActionがコールされた際、読み込みにいく対象(text/htmlリクエスト)を示す。
sourceFrameとは、decidePolicyForNavigationAction navigationActionがコールされた際、遷移元を示す。
ただ,sourceFrameはnilになることが多い。ページ遷移時最初にコールされるdecidePolicyForNavigationAction navigationAction内ではtrueになるが、
それ移行、ページ内部のコンテンツを読み込む際にコールされる時(targetFrameのmainFrameプロパティがfalseのとき)にはnilになるようだ。
ただし、リダイレクトで遷移したときに関してはtargetFrameのmainFrameプロパティがtrueであっても、sourceFrameの値はnilになる。
targetFrameに関しては基本的にはnilにならないが、target=_blankで遷移したときのみnilになる。

WKUIDelegate

このプロトコルは、ウェブページの代わりにネイティブユーザインターフェース要素を提示するための方法を提供する。

webView(_:createWebViewWithConfiguration:forNavigationAction:windowFeatures:)

このメソッドは既存のWKWebView内で新しいウィンドウやフレームを指定してコンテンツが開かれようとしているときに呼ばれる。

webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:
webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:
webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:

以上のメソッドはそれぞれjavascriptのalert,confirm,promptが実行された際に呼ばれる。

注意点

JavaScriptのalert,confirm,promptを表示させるにはWKUIDelegateの実装が必要

WKUIDelegateプロトコルのデリゲートメソッドを実装し、その中でネイティブのアラートなどを使用して表示する必要がある。

target="_blank"のaタグが無反応

何故かblank(=新規ウィンドウで表示)のリンクを押しても反応しない、そのため多少工夫をする必要がある。
2つの解決方法の案が存在する。
(1つ目)
UINavigationDelegateのdecidePolicyForNavigationやWKUIdelegateのwebView:createWebViewWithConfiguration:メソッドを実装し、
その中で遷移予定のurlとWKNavigationActionオブジェクトが保持しているtargetFrameを取得する。
blankのリンクならばtargetFrameが存在しない(=nil)となっているので、もしそうならばWKWebViewのオブジェクトからloadRequestメソッドを用いて
遷移予定のurlに遷移させれば良い。(これなら1つのWKWebViewで遷移が可能)
(2つ目)
WKUIDelegateのwebView:createWebViewWithConfiguration:メソッドを実装する。
その中で新しくWKWebViewのインスタンスを作成し、addSubViewした後、WKWebViewのインスタンスを返す。

たぶん2つ目の方法が正攻法。当然新しくWKWebViewを作成するので、スワイプによる戻る、進むは元のWKWebViewとは共有しない。

IBから生成できない

今のところはコードで生成しないといけない

他アプリへのURLスキームやAppStoreへのリンクは自分で振り分けないといけない

解決方法の案を示す
WKNavigationDelegateプロトコルのdecidePolicyForNavigationAction(遷移許可を決めるメソッド)を実装
遷移予定のurlの文字列を取得
正規表現を用いてそのurlがappStoreへのリンクか、他アプリへのURLスキームかを判断する
もしappStoreへのリンクや他アプリへのURLスキームならば、UIApplication.sharedApplication().openURL:を実行
decisionHandler(.Cancel)とし、遷移をキャンセルする
ただ、swiftでは手軽に正規表現を利用できないため少し難しい。

Basic認証がかかったサイトへアクセスできない

Basic認証がかかったサイトは遷移するためには、WKNavigationDelegateの
webView(_:didReceiveAuthenticationChallenge:completionHandler:)メソッドを実装する必要がある。
ネイティブでusernameやpasswordを入力させるアラートを作成し、入力された情報をNSURLCredentialにセットしてcompletionHandlerを呼び出す。

調査事項

リダイレクトに関して

調査方法

WKNavigationDelegateのメソッドを全て実装して、どの順番で呼び出されているのかを確かめる。
まずは通常時どの順番で呼び出されているのかを記録し、次にリダイレクト時にはどの順番でどのメソッドが呼ばれているのかを記録し、比較する。

調査結果

調査対象のメソッドは以下の通り。
(WKUIDelegate)

webView(_:createWebViewWithConfiguration:forNavigationAction:windowFeatures:)

(WKNavigationDelegate)
webView(_:didCommitNavigation:)
webView(_:didFailNavigation:withError:)
webView(_:didFailProvisionalNavigation:withError:)
webView(_:didFinishNavigation:)
webView(_:didReceiveAuthenticationChallenge:completionHandler:)
webView(_:didReceiveServerRedirectForProvisionalNavigation:)
webView(_:didStartProvisionalNavigation:)
webView(_:decidePolicyForNavigationAction:decisionHandler:)
webView(_:decidePolicyForNavigationResponse:decisionHandler:)

(通常時)

webView(_:decidePolicyForNavigationAction:decisionHandler:) //最初のコール

webView(_:didStartProvisionalNavigation:)

webView(_:decidePolicyForNavigationResponse:decisionHandler:)

webView(_:didCommitNavigation:)

webView(_:didFinishNavigation:) //最後のコール

(リダイレクト時)

webView(_:decidePolicyForNavigationAction:decisionHandler:) //最初のコール

webView(_:didStartProvisionalNavigation:)

webView(_:decidePolicyForNavigationAction:decisionHandler:)

webView(_:didReceiveServerRedirectForProvisionalNavigation:)

webView(_:decidePolicyForNavigationResponse:decisionHandler:)

webView(_:didCommitNavigation:)

webView(_:decidePolicyForNavigationAction:decisionHandler:)

webView(_:didFinishNavigation:) //最後のコール

という結果となった。
_:decidePolicyForNavigationAction:decisionHandler:
_:decidePolicyForNavigationResponse:decisionHandler:はtext/htmlリクエストの回数に依存するのでこの限りではない。

webView(_:didReceiveServerRedirectForProvisionalNavigation:)より前に確実に一度だけ呼び出されるメソッドは
webView(_:didStartProvisionalNavigation:)である。