CordovaのUIWebView→WKWebView移行についてまとめ


2021/03/16追記
アップデートについては期限が延長されました(新しい期限は未定です)
https://developer.apple.com/news/?id=edwud51q

UIWebViewを使用している場合、新規リリースは2020/4、アップデートは2020/12からApp Storeがアプリを受け付けなくなります。
https://developer.apple.com/news/?id=12232019b

代替手段としてWKWebViewがあります。
UIWebViewからWKWebViewに移行するに当たり、手順やハマった点をまとめます。(主にCordovaにおいてのノウハウですが、Cordovaに限らない内容も記載します)

同時期にXcode11のビルドも必須になりますが、それについては以下投稿を参照ください。
https://qiita.com/kishisuke/items/b1c0096dc832972da9be

他に何かあったら随時追記します!

UIWebViewを利用しているか確認する方法

UIWebViewを実際に画面に表示していなくとも、リンクしている場合はリジェクトされる可能性があります。
UIWebViewがリンクされているかは、以下のコマンドで確認可能です。

nm <バイナリファイルのパス> | grep UIWebView

※バイナリファイルのパスは以下で確認。

  • ipaファイルの場合
    • ipaファイルを解凍する(拡張子をzipに変更して、zip形式として解凍)
    • {ファイル名}/Payload/{ファイル名}.app/{ファイル名}
  • appファイルの場合
    • {ファイル名}.app/{ファイル名}

以下はUIWebViewがリンクされているリリースビルドのipaファイルを確認した際の結果例です。
何も表示されなかったらOKです。

U _OBJC_CLASS_$_UIWebView
U _OBJC_METACLASS_$_UIWebView
        U _OBJC_CLASS_$_UIWebView
        U _OBJC_METACLASS_$_UIWebView

また、UIWebViewがリンクされているアプリをAppStoreにアップロードすると、以下の警告メールが届きます。(2020/02/19時点)

ITMS-90809:非推奨のAPIの使用-AppleはUIWebView APIを使用するアプリの提出を受け付けなくなります。詳細については、https://developer.apple.com/ documentation / uikit / uiwebviewを参照してください。

基本的な対応方法

WebViewのEngineをWKWebViewに変更

Cordovaは標準のEngineとして、UIWebViewを利用しています。
WKWebViewを利用するには、cordova-plugin-wkwebview-engineをインストールします。

ただ、EngineをWKWebViewに変えただけでは、UIWebViewはリンクされたままです。
UIWebViewをリンクしなくするには、Cordova iOSを5.1.1にアップデートして、config.xmlのWKWebViewOnlyをtrueに設定してください。

config.xml
<preference name="WKWebViewOnly" value="true"/>

このフラグをONにすることで、プリプロセッサでUIWebViewを参照しているコードが除去されます。なお、次期メジャーバージョンで完全にUIWebViewのコードを除去して、プラグインなしでWKWebViewを標準のEngineとして採用するとのことです。
https://github.com/apache/cordova-discuss/pull/110#issuecomment-573036332

Note: 2020/02/19時点ではMonacaはCordova iOS 5.1.1に対応していません。対応を待ってからアップデートしましょう。

Note: 2020/03/03追記 MonacaもCordova iOS 5.1.1&WKWebViewに対応しました!詳細は以下記事を。

2020年4月以降のiOSアプリ申請について - Qiita
WKWebViewサポートを開始 | モナカプレス

Note: 2021/03/16追記 Cordova iOS 6.0.0でWKWebView Engineが本体に統合されました。そのため、Cordova iOS 6.0.0を利用する場合は上記の手順は不要です。

サードパーティプラグインの対応

サードパーティのプラグインでもUIWebViewをリンクしている可能性があります。
その場合、プラグインをアップデートまたは削除(必要であれば代替実装に移行)します。

以下は対応例です。

InAppBrowser

InAppBrowserはWKWebViewに対応しており、UIWebViewの非リンク化も3.2.0から対応しています。

なお、InAppBrowserを3.2.0にアップデートすると、Androidの場合にフルスクリーンで表示されるようになります(ステータスバーが非表示になる)。以下のプルリクエストでフルスクリーンをOFFにできるようになりましたが、まだリリースされていません。
https://github.com/apache/cordova-plugin-inappbrowser/pull/634

とりあえずフルスクリーンを回避する場合は、以下の行をコメントアウトします。
https://github.com/apache/cordova-plugin-inappbrowser/blob/3.2.0/src/android/InAppBrowser.java#L797

Firebase SDK

6.8.0と6.8.1からUIWebViewの非リンク化に対応しています。
6.8.1未満の場合は、アップデートしましょう。

AFNetworking

2020/02/19時点(3.2.1)ではUIWebViewをリンクしています。プルリクが作成されており、もう少し経つとマージされるかもしれません。
https://github.com/AFNetworking/AFNetworking/pull/4439

ハマった点

LocalStorageなどのデータ移行

UIWebViewの場合、LocalStorageやWebSQL、IndexedDBなどのブラウザに永続化するデータは、アプリのデータ領域にファイルとして保存されています。
WKWebViewも同様ですが、ファイルの保存先がUIWebViewと異なるため、単純にUIWebViewからWKWebViewにアップデートすると、WebView上のJavaScriptから見るとデータがロストした様に見えます。
そのため、元々UIWebViewを埋め込んでいたアプリをWKWebViewにアップデートする場合は、LocalStorageなどのデータ移行が必要になります。(LocalStorageなどをキャッシュ情報としてしか利用していない場合はその限りではありません)

LocalStorageに限れば移行するCordovaプラグインがあります。実際にiOS 13.3で確認したところ、データが移行できていました。(実際に使用する際は、各OSバージョンごとに検証するなどしてください)
https://github.com/MaKleSoft/cordova-plugin-migrate-localstorage

2020/04/06追記
UIWebViewはLibrary/WebKit/LocalStorage/file__0.localstorageに、WKWebViewはLibrary/WebKit/WebsiteData/LocalStorage/file__0.localstorageにそれぞれデータが保存されています。このプラグインはWKWebViewのファイルが無く、UIWebViewのファイルが有る場合に1回だけUIWebViewのファイルをWKWebViewのファイル保存先にコピーします。

なお、ローカルストレージのファイル名がオリジン名+"0.localstorage"となっている点に注意してください。このプラグインはfileスキームにしか対応しておらず、後述のCustom Schemeプラグインcordova-plugin-ionic-webviewを使っているとファイル名が変わります。
例えば、Custom Schemeプラグインの場合、ローカルストレージのファイル名はmonaca-app_monaca.io_0.localstorageになります。

fileスキーム以外を使用している場合、以下の箇所を書き換えて利用しましょう。
https://github.com/MaKleSoft/cordova-plugin-migrate-localstorage/blob/master/src/ios/MigrateLocalStorage.m#L52

※cordova-plugin-ionic-webviewの場合は、Ionicチームがforkしているリポジトリを使うと良いです。

CordovaでXHRがエラーになる

file://スキームからHTTP(S)へのアクセス

CordovaのWebアプリ(index.html)はfile://スキームでアクセスします。そのため、例えばfile://スキームからインターネット上で公開されているHTTPSのAPIにアクセスしようとすると、オリジンが異なるためエラーになります。
UIWebViewではこの制約は無視されていましたが、WKWebViewではブラウザと同様にクロスドメイン通信を行う場合はCORSに対応する必要があります。

対応方法は以下記事などを参照してください。
https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34

file://スキームへのアクセス 2020/03/23追記

WKWebViewでは、file://スキームのローカルリソース(wwwディレクトリ配下のファイル)にXHRでアクセスすることができません。例えば、AngularJS(1.x系)を使用している場合、テンプレート(html)を外部ファイル化しているとXHRで取得できなくなります。

この問題はcordova-plugin-wkwebview-file-xhrを使用することで回避できます。このプラグインはXHRの処理をフックして、ネイティブ側(NSURLSession)でHTTPリクエストを行い、結果を返します。
InterceptRemoteRequestsnoneに設定することで、file://スキームへのリクエストのみプラグインで処理することも可能です。
注意点ですが、Cordovaプラグインのため、プラグインの初期化後(devicereadyイベント)でないとリソースの取得ができません。もし、プラグインの初期化前にローカルリソースをXHRで取得している場合は修正が必要です。

また、Vue.jsやReactなどのフレームワークを使用すると、AngularJSでいうところのテンプレートファイルはwebpackによって1つのJSファイルにまとめることができるため1、ローカルリソースにXHRでアクセスする必要がありません。これからアプリを開発するのであれば、ローカルリソースにXHRでアクセスする必要のないフレームワークやライブラリを採用することをおすすめします。

Monacaの場合 2020/04/01追記

以下記事に書いている通り、Monacaから提供されている「Custom Schemeプラグイン」でも対応可能です。
https://qiita.com/kishisuke/items/df8b20a78e9d6843ab01

2021/03/16追記

Cordova iOS 6.0.0でMonacaの「Custom Schemeプラグイン」相当の機能が標準実装されました。
https://docs.monaca.io/ja/release_notes/20201119_cordova10/

WKWebViewのUserAgentを変更する方法

以下の通り、3つの方法があります。

  1. UserDefaultsで設定
  2. WKWebView#customUserAgent で設定
  3. WKWebViewConfiguration#applicationNameForUserAgentで設定

https://blog.anzfactory.xyz/articles/20190902/swift-wkwebview-custom-useragent/ を参考にさせていただきました。

この時、優先順は2>1>3です。
1と2は設定したUAがそのまま送信されるが、3は元々のUAの末尾に付く。
1と2で3と同じ様にしたい場合は、自身で元々のUAを取得する必要がある(UAはUIWebViewと同様にJavaScriptを実行して取得する必要があるが、WKWebViewのJS実行処理は非同期のため扱いづらい)
と言う点が異なります。

3の方法が良さそうですが、他のライブラリが1でUAを設定している場合、優先順の関係で適用されない問題があります。実はCordovaが独自に1の方法でUAを設定しているため、3の方法で設定したUAが無視されてしまいました😇

そこで、以下の方法で対応しました。

  • 1の方法でUAが設定されている場合は、1の方法でUAを書き換え。
  • それ以外の場合は3の方法でUAを設定する。

以下サンプルです。

NSString *originalUserAgent = [[NSUserDefaults standardUserDefaults] stringForKey:@"UserAgent"];
if (originalUserAgent != nil && originalUserAgent.length > 0) {
    NSString *newUserAgent = [self editUserAgent: originalUserAgent]; // 何らかの編集処理
    [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"UserAgent": newUserAgent }];
}

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.applicationNameForUserAgent = @"AppName";
self = [super initWithFrame:CGRectZero configuration:configuration];

ちなみに、CordovaはUAを以下の様にして無理やり同期的に取得しています。(JSが実行完了するまで、メインスレッドをブロック)
1と2の方法で対応する場合で、非同期にUAを取得するのが大変な場合は、参考になるかもしれません。
https://github.com/apache/cordova-ios/blob/e8a041ef949309b1f23dfc7a03de0f019c831359/CordovaLib/Classes/Public/CDVUserAgentUtil.m#L44-L65

Cordova以外でハマった点

POSTの問題

Content-Typeの設定

iOS13でWKWebViewでボディをPOSTする場合は、Content-Typeヘッダーを明示的に設定する必要があります。(iOS12まではapplication/x-www-form-urlencodedの場合、Content-Typeヘッダーの設定は不要でした)

NSString *url = @"";
NSMutableString *body = @"name=value";

NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
[req setHTTPMethod:@"POST"];
[req setHTTPBody:[body dataUsingEncoding: NSUTF8StringEncoding]];
[req addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; // iOS 13から必須

[self loadRequest:req];

ボディが空になるバグ

iOS9〜10のWKWebViewでPOSTするとボディが空になるバグがあります。
https://www.lanches.co.jp/blog/8953

以下投稿などの対応方法がありますが、Cookieでセッションを生成しているなどの場合に対応できない可能性があります。
https://qiita.com/shunkitan/items/a23b0b109f434a28ab8b#1-wkwebview%E3%81%A7%E3%81%AFpost%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%AEhttp-body%E3%81%8C%E7%84%A1%E8%A6%96%E3%81%95%E3%82%8C%E3%82%8B

iOS11でバグは修正されているため、対応するOSをiOS11以降とするのも手です。
2020/02/19時点で94%のiPhoneユーザーはiOS12以降を利用しており、現実的な手段だと思います。

WKWebViewに移行すると良いこともある

対応方法だけ書くと面倒なだけに感じますが、WKWebViewにすることで大きなメリットもあります。

例えば、引っ張って更新するUIを実装するとflickingするバグなどはWKWebViewにすると直ります。UIWebViewの謎挙動に悩まされてきた人多いんじゃないでしょうか(私です)

その他にも全体的にパフォーマンスが向上したり(特にスクロール)、機能的にもSign in with Appleが動くなど恩恵があるので、是非この機会に頑張って対応しましょう!


  1. Vue.jsやReactの場合、単一ファイルコンポーネント(Vue.js)やJSXで書かれたHTML(の様なコード)は、webpackを通じてJavaScriptの仮想DOMツリーに変換されます。