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

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

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

代替手段として[WKWebView](https://developer.apple.com/documentation/webkit/wkwebview)があります。
UIWebViewからWKWebViewに移行するに当たり、手順やハマった点をまとめます。（主にCordovaにおいてのノウハウですが、Cordovaに限らない内容も記載します）

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

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

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

```shell
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](https://github.com/apache/cordova-plugin-wkwebview-engine)をインストールします。

ただ、EngineをWKWebViewに変えただけでは、UIWebViewはリンクされたままです。
UIWebViewをリンクしなくするには、Cordova iOSを[5.1.1](https://cordova.apache.org/announcements/2019/12/02/cordova-ios-release-5.1.1.html)にアップデートして、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](https://qiita.com/keeey/items/d591aa212992b3281458#wkwebview-%E5%AF%BE%E5%BF%9C)
[WKWebViewサポートを開始 | モナカプレス](https://press.monaca.io/takuya/4767)

**Note:** 2021/03/16追記 [Cordova iOS 6.0.0](https://cordova.apache.org/announcements/2020/06/01/cordova-ios-release-6.0.0.html)でWKWebView Engineが本体に統合されました。そのため、Cordova iOS 6.0.0を利用する場合は上記の手順は不要です。

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

以下は対応例です。

#### InAppBrowser
InAppBrowserはWKWebViewに対応しており、UIWebViewの非リンク化も[3.2.0から対応](https://cordova.apache.org/announcements/2020/01/08/inappbrowser-release-3.2.0.html)しています。

なお、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](https://firebase.google.com/support/release-notes/ios?hl=ja#version_681_-_september_10_2019)から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プラグイン](https://qiita.com/kishisuke/items/df8b20a78e9d6843ab01)や[cordova-plugin-ionic-webview](https://github.com/ionic-team/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しているリポジトリ](https://github.com/ionic-team/cordova-plugin-migrate-localstorage)を使うと良いです。

### 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](https://github.com/oracle/cordova-plugin-wkwebview-file-xhr)を使用することで回避できます。このプラグインはXHRの処理をフックして、ネイティブ側（NSURLSession）でHTTPリクエストを行い、結果を返します。
`InterceptRemoteRequests`を`none`に設定することで、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を設定する。

以下サンプルです。 

```objc
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ヘッダーの設定は不要でした）

```objc
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以降を利用しており](https://developer.apple.com/support/app-store/)、現実的な手段だと思います。

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

例えば、[引っ張って更新するUIを実装するとflickingするバグ](https://speakerdeck.com/kishino/monacaahuriwoneiteihufalseuxnijin-tukerutameni?slide=30)などはWKWebViewにすると直ります。UIWebViewの謎挙動に悩まされてきた人多いんじゃないでしょうか（私です）

その他にも全体的にパフォーマンスが向上したり（特にスクロール）、機能的にも[Sign in with Apple](https://qiita.com/kishisuke/items/e83112d06e0d907a956f#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9Ajs%E7%89%88%E3%82%92cordova%E3%81%AB%E5%9F%8B%E3%82%81%E8%BE%BC%E3%82%80%E3%81%A8)が動くなど恩恵があるので、是非この機会に頑張って対応しましょう！

[^1]: Vue.jsやReactの場合、[単一ファイルコンポーネント](https://jp.vuejs.org/v2/guide/single-file-components.html)（Vue.js）やJSXで書かれたHTML（の様なコード）は、webpackを通じてJavaScriptの仮想DOMツリーに変換されます。