経緯
UIWebViewの使用経験が長かったこともあり、WKWebViewを使う際にそんな苦労しないし余裕でしょ!って、舐めてかかったら痛い目を見たので今更な内容ではあるが、自戒を込めてまとめました。
正直、ちゃんとドキュメント読めばいくつかは最初から気づけたので、ちゃんと読めという話です。
開発環境
- Xcode 9.2
- Swift 4.0
- iOS Deployment Target 9.0
- 画面はStoryboardで実装できる箇所は全てStoryboardに寄せて作成
- WKWebViewだけはコード実装
- WhiteList(配列に持っているドメイン名)の文字列のみ、WKWebViewで表示するのを許可し、他はSafariに飛ばす仕様
1. Error用Delegateメソッドを1つしか実装していなかった
発生した問題
- Error用のDelegateメソッドにBreak Pointを張っていたが、端末を機内モードにしてもBreak Pointで止まらなかった
原因
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)
しか実装していなかった。
調べたらもう1つError用のDelegateメソッド func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)
があり、こちらでErrorが流れてくるようになっていた。
補足
ドキュメントを見ると用途が以下のように違うことが記載されている
-
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)
- WebPageの読み込み途中でerrorが起きた時に呼ばれる
- 発生例 -> ページ読み込み中にリンクをタップして読み込みをキャンセルした時等
- WebPageの読み込み途中でerrorが起きた時に呼ばれる
-
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)
- WebPageの読み込み開始時にerrorが起きた時に呼ばれる
- 発生例 -> 圏外によりページを読み込めたかった時等
- WebPageの読み込み開始時にerrorが起きた時に呼ばれる
エラーの発生状況でどちらのDelegateメソッドにもErrorが来る可能性はあるため、両方実装した上で適切にハンドリングしてやる必要がある
2. ローカルのError時用のHTMLを読み込んだらreloadで復帰できなくなった
発生した問題
- ローカルにあるerror.htmlをエラー発生時にWKWebViewに読み込ませたら、
goBack()
、goForward()
時に正常なWebページの合間にerror.htmlが表示されるようになった。 -
reload()
でerror.htmlが繰り返し読み込まれ、error前にURLRequestで投げたページにアクセスしようとしなくなった。
原因
ローカルにあるHTMLを読み込む方法は複数あるが、fileプロトコルでHTMLを読み込んでいたため、WKWebViewのHistoryにStackされるようになっていた。
HTMLを文字列としてWKWebViewに読み込ませることで、HistoryにStackさせなくすることができる。
補足
- fileプロトコルで読み込み
- WebViewのurlに
file:///...
のフォーマットで入る - urlがhistoryにstackされ、
reload()
やgoBack()
、goForward()
で読み出される - url.isFileでtrueが返るようになる(ローカルからの読み出し判別が可能)
- WebViewのurlに
let errorHtmlPath = Bundle.main.path(forResource: "error", ofType: "html")!
let url = URL(fileURLWithPath: errorHtmlPath)
webView.load(URLRequest(url: url))
- HTMLを読み込み(baseURLにnilを入れる場合のみ)
- WebViewのurlには
about:blank
が入る - historyにstackされないので、
reload()
やgoBack()
、goForward()
で読み出されない - 意図的に
about:blank
を表示した場合と区別つけられない(ローカルのからの読み出し判別が不可能)
- WebViewのurlには
let errorHtmlPath = Bundle.main.path(forResource: "error", ofType: "html")!
let htmlString = try! String(contentsOfFile: errorHtmlPath)
webView.loadHTMLString(htmlString, baseURL: nil)
3. リンクをタップしても読み込まなかったり、意図せずSafariに飛ばすことになった
発生した問題
- リンクをタップしても読み込みを開始しない
- WhiteListに含まれるページにも関わらず、ページ内にあるFacebookやTwitterのボタンのURLがトリガーとなってSafariに遷移する
原因
URLの読み込みを許可するか許可しないかを決定できるDelegateメソッド
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
内で行うハンドリングに問題があった。
navigationAction.navigationType
でリンクをタップした時のURLなのか、goBack()
、goForward()
、reload()
で読み込まれたURLなのか等を判別できるが、一律同じ処理をしていたため問題が発生していた。
(特にリンクタップ時のlinkActivated
とリンクタップ後ページ内にある他の読み込み必要なURLやリダイレクトの時のother
を同一として扱うが良くなかった。)
補足
最終的には、このようなコードでハンドリングすることになった。
extension WebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// navigationTypeに応じて処理を分岐
switch navigationAction.navigationType {
case .linkActivated:
linkActivated(webView: webView, navigationAction: navigationAction, decisionHandler: decisionHandler)
case .backForward, .formResubmitted, .formSubmitted, .reload, .other:
othersAction(webView: webView, navigationAction: navigationAction, decisionHandler: decisionHandler)
}
}
func linkActivated(webView: WKWebView, navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// リンクをタップした時のみの処理
// タップした遷移先のURLを取得
guard let url = navigationAction.request.url else {
decisionHandler(.cancel)
return
}
// 遷移先をwebviewで表示していいドメインかチェック
guard allowLoadUrl(url: url) else {
// WhiteListにないドメインなのでSafariに飛ばす
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
// WebViewでの表示許可
decisionHandler(.allow)
}
func othersAction(webView: WKWebView, navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// リンクタップ以外の処理(戻る、進む、リダイレクト等)
// MainFrameのRequestか判定
guard let isMainFrame = navigationAction.targetFrame?.isMainFrame, isMainFrame else {
// リンクタップ時のメインフレーム以外はここに流れるので全て許可(FacebookやTwitterのURL諸々)
decisionHandler(.allow)
return
}
// リダイレクト等の遷移先のURLを取得
guard let url = navigationAction.request.url else {
decisionHandler(.cancel)
return
}
// 遷移先をwebviewで表示していいドメインかチェック
guard allowLoadUrl(url: url) else {
// WhiteListにないドメインなのでSafariに飛ばす
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
// WebViewでの表示許可
decisionHandler(.allow)
}
}
参考にしたURL:
4. Storyboard上でiPhone 8の4.7inchサイズを表示して作っていたら他の画面サイズの時にWKWebViewのサイズがおかしくなった
発生した問題
iPhone 8等の4.7inchサイズの時はWKWebViewが正常に意図通りのサイズで表示されるが、他の画面サイズで動作させた際に、意図しないWKWebViewのサイズで表示された。
原因
Storyboard上の表示をView as: iPhone 8
にしてUIを作っていたため、各Viewの初期サイズがiPhone 8の画面サイズで生成されていた
また、WKWebViewにAutoLayoutの制約をつけ忘れていた。
補足
WKWebViewを下記のように生成と配置をしていた。
@IBOutlet var webViewBase: UIView
lazy var webView = WKWebView(frame: webViewBase.frame)
func viewDidLoad() {
super.viewDiDLoad()
webViewBase.addSubview(webView)
webView.navigationDelegate = self
}
lazy var
でwebViewを遅延生成しているものの初回で呼ばれるviewDidLoad()
でwebViewBaseのサイズを元にwebViewが作られる。
しかし、このタイミングでは、AutoLayoutの制約が反映される前のサイズのため、画面が表示された時には、意図しないサイズでwebViewが表示される。
AutoLayoutの制約が反映されるタイミングは、LifecycleのviewDidLayoutSubviews()
で、その前にviewのサイズを取得しても制約により変動する可能性がある。
// Lifecycle
viewWillAppear
viewWillLayoutSubviews // AutoLayoutの調整開始前
viewDidLayoutSubviews // AutoLayoutの調整完了
viewDidAppear
4.7inch端末で意図通りのサイズで表示されていた理由は、Storyboard上のView as: iPhone 8
の表示しているサイズに依存していたため。
WKWebViewに制約をついていないことが問題だったので、viewDidLoad()
で下記のように制約をつけてやることでどの画面サイズでも意図した見た目で表示された。
// webViewの制約設定時、AutoresizingMaskによって自動生成される制約と競合するため、自動生成をやめる
webView.translatesAutoresizingMaskIntoConstraints = false
// webViewの制約
NSLayoutConstraint.activate([webView.leadingAnchor.constraint(equalTo: webViewBase.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: webViewBase.trailingAnchor),
webView.topAnchor.constraint(equalTo: webViewBase.topAnchor),
webView.bottomAnchor.constraint(equalTo: webViewBase.bottomAnchor)])
注意
1つ注意したいのが、webView.translatesAutoresizingMaskIntoConstraints = false
を行わないとWKWebViewで制約決める時に他の制約とぶつかりUnable to simultaneously satisfy constraints.
がログに出る。
これはAutoresizingMask
が設定されていると自動的にAutoLayoutの制約に置き換えてくれるのだが、defaultでtranslatesAutoresizingMaskIntoConstraints
はtrue
になっているため、false
を設定してやらないといけない。