WKWebViewに関する調査メモ

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ページの画面遷移を監視し、フックする事ができる。

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

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

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

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

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

```swift
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値

```swift
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プロパティから取得できる。

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

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

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

たぶん２つ目の方法が正攻法。当然新しく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)

```swift
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:)
```

（通常時）

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

webView(_:didStartProvisionalNavigation:)

webView(_:decidePolicyForNavigationResponse:decisionHandler:)

webView(_:didCommitNavigation:)

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

（リダイレクト時）

```swift
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:)``である。