50
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WKWebViewで新しいウィンドウ(タブ)を開く

Last updated at Posted at 2018-07-08

ついにというかようやくというかUIWebViewがiOS12でdeprecatedとなりました!
それに伴い、これまでしぶとくUIWebViewを使っていたアプリもWKWebViewへと移行せざるを得なくなってきたかもしれません。

それで、UIWebViewからの移行で困るポイントの一つである「新しいウィンドウが開けない問題」をまとめ直してみました。

WKWebViewで新しいウィンドウ(タブ)を開くには

UIWebViewでは、新しいウィンドウ(タブ)を開くリンクをタップすると、同じWebViewの中でそのまま遷移していました。

ところが、WKWebViewではそういったリンクをタップしても無視される為、新しいウィンドウ(タブ)を開くリンクがタップされた時にどういった処理をするのかをアプリ側で実装する必要があります。

(ちなみに、UIWebViewだけでなくSFSafariViewController、AndroidのWebViewも同じWebView内でそのまま遷移します)

なお、HTMLで新しいウィンドウ(タブ)を開く方法には以下の方法があります。

  • <a><form>タグなどでtarget="_blank"を指定する
  • javascriptでwindow.openを実行する

対応方法

ベストな対応方法は、新しいウィンドウ(タブ)を開くようなWebの構成は、スマホのブラウザとは相性が悪いのでやめてしまうというものです。

次点として、単にアプリからWebサイトを開かせたいといった場合はWKWebViewではなくSFSafariViewControllerを使う方が良いです。

とは言っても、そうもいかないこともある1ので、その場合は実装で対応することになります。。。

対応方法1:同じWebView内で開く

最も一般的な方法はUIWebViewの挙動を踏襲して、新しい画面で開くページを元の画面と同じWebViewでそのまま開くというものです。

(QiitaでもWKWebViewでtarget="_blank"なリンクが開かない時の対処法という記事があります)

実装としては、WKUIDelegatewebView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:を以下のように実装し、WKWebViewuiDelegateにセットします。

func webView(_ webView: WKWebView,
             createWebViewWith configuration: WKWebViewConfiguration,
             for navigationAction: WKNavigationAction,
             windowFeatures: WKWindowFeatures) -> WKWebView?
{
    if navigationAction.targetFrame?.isMainFrame != true {
        webView.load(navigationAction.request)
    }
    
    return nil
}

ただし、この方法には落とし穴が3つあります。

問題点1.1:元の画面のページを上書きして読み込んでしまう

先ほどの記事のコメント欄で指摘されている点です。

target="_blank" を強引にメインのwebViewにロードさせる場合は注意が必要です。

例えばイベントサイトのATNDで参加登録をした時、
メインフレームで参加登録リクエストが投げられ、別ウィンドウでTwitterへの投稿画面が開かれようとします。

このとき、別ウィンドウで開かれようとしているTwitterへの投稿画面を、
参加登録リクエストを送信するはずだったwebViewに読み込ませてしまうと、
肝心の参加登録処理が実行されないままになってしまいます。

なお、この問題は他のWebViewの場合でも発生します。

問題点1.2:window.close()ができない

よくあるポップアップ画面だと、元の画面に戻りやすい様に「閉じる」ボタンがついています。そして「閉じる」をタップするとjavascriptでwindow.close()を呼び出し、新しく開いた画面を閉じる仕組みになっています。

ただ当然ながら、同じ画面で開いている場合は、新しく開いた画面が存在しないのでwindow.close()はエラーとなってしまいます。

なお、この問題も他のWebViewの場合でも発生します。

問題点1.3:POSTに対応できない

例えば、以下のように<form>タグでPOSTが使われていた場合、フォームのパラメータは送信されません。

<form action="https://〜" method="post" target="_blank">...</form>

これはwebView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:navigationActionhttpBodyが、POSTのデータがある場合でも必ずnilとなってデータが欠落してしまうからです。

ちなみにWKNavigationDelegatewebView:decidePolicyForNavigationAction:decisionHandler:でも、httpBodynilになります。こちらの方はバグレポート2があります。

意図的な実装なのかも知れませんが、いずれにせよどうしようもないので、この落とし穴を回避するには他の方法を採用する必要があります。

対応方法2:新しいWebViewで開く

普通のブラウザの様に新しいWebViewを作成してそこで新しいページを読み込むという方法です。

この方法であれば、

  • 元の画面のページを上書きして読み込んでしまう
  • window.close()ができない

の問題は解決されます。

実装としては、先ほどの同じWebViewで開く場合と少し違って、以下のように新しいWKWebViewを生成して返します。

func webView(_ webView: WKWebView,
             createWebViewWith configuration: WKWebViewConfiguration,
             for navigationAction: WKNavigationAction,
             windowFeatures: WKWindowFeatures) -> WKWebView?
{

    if navigationAction.targetFrame?.isMainFrame != true {
        let newWebView = WKWebView(frame: webView.frame,
                                   configuration: configuration)
        newWebView.load(navigationAction.request)
        newWebView.uiDelegate = self
        webView.superview?.addSubview(newWebView)
        return newWebView
    }
    
    return nil
}

この実装はサンプルなので単に同じサイズや位置で新しいWKWebViewを生成していますが、実際はちゃんとAutoLayoutの制約をはった方が良いです。
また、ポップアップのようにサイズが指定されていた場合は、windowFeaturesにサイズなどが入っていますので、これを使うこともできます。

さらに、window.close()が呼ばれた時に正しく画面が閉じた状態となるよう、以下の実装をします。

func webViewDidClose(_ webView: WKWebView) {
    webView.removeFromSuperview()
}

ただし、この方法にも2つの落とし穴があります。

問題点2.1:UIをどう実装するか

例えば、サンプルのように元のWebViewと同じ位置に同じサイズで重ねてしまうといったシンプルな実装の場合、どうやって新しい画面を閉じさせるのか?という問題が出てきます。

もし、Web画面の中に必ずwindow.close()のボタンがあれば問題ありませんが、無い場合は閉じるボタンの様なUIをアプリ側に実装する必要があります。

当然、Safariみたく真っ当なUIを実装しようとすると、かなりの工数がかかります・・・

問題点2.2:メモリの枯渇問題

WKWebViewはそれなりにメモリを消費します。
新しく開いたWebViewの中にさらに新しい画面で開くリンクがあったりすると、延々と新しいWebViewが生成されることとなり、やがてはメモリが枯渇します。

ただ、これらの落とし穴はWeb側の作りによっては考慮しなくて良い場合もあります。ですが、多分、この方法で解決することはあまり無いと思います・・・

という訳で、残るは最後の手段です。

対応方法3:スクプリトで書き換える

WKWebViewはページのロードが完了した時に、任意のjavascriptを実行させることができます。
それで、元のページに対してスクリプトを実行し、問題となる部分を書き換える、という考え方です。

どういったスクリプトを実行させるかは、表示したいページの挙動に合わせる必要があります。

例えば、シンプルに対応する場合だと、次のような実装ができます。

func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
    let script = """
                    var elements = document.body.getElementsByTagName("*");
                    for (var i = 0; i < elements.length; i++) {
                        if (elements[i].target == '_blank') {
                            elements[i].target = '_self'
                        }
                    }
                 """
    webView.evaluateJavaScript(script, completionHandler: nil)
}

実行するスクリプトの内容は、全てのタグを調べてtarget_blankなら_selfへ書き換えて新しいウィンドウを開かないようにしてしまうものです。

他にも、

  • window.close()を書き換えて戻る動作をさせる
  • ポップアップのイベントを拾って処理を行う

など、Web側の実装に合わせてスクリプトを実装することができます。

(ただ、こういった実装をするぐらいなら、Web側で対応する方が良いですが・・・)

まとめ

網羅的にまとめたので長くなってしまいましたが、単に既存と合わせるだけなら対応方法1の同じWebViewで開かせる方法で十分です。

それ以上の対応が必要なら、Web側で対応してもらった方が良いです。

ですが、諸事情によりWeb側での対応ができないなら、ここに挙げた方法のどれかで解決できることを願っています・・・

  1. カスタムUAを使いたいとかjavascriptを実行させたいとかPOSTを使いたいとか埋め込みのWebViewで使いたいとか・・・

  2. HTTP body/bodystream (e.g., POST form data) is missing in WKNavigationDelegate callbacks

50
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?