【React Native】Expo製のiOSアプリでService Workerを使う

以前開発していたExpo製のアプリでService Workerを使用することになったのですが、Androidでは問題なく動作するのに対して、iOSではService Workerが動作せずハマりました。
iOSのWKWebViewではデフォルトでService Workerが使用できないようになっているのが原因だったようで、わかってしまえば簡単ですが、WKWebViewでService Workerを使用するための設定や手順で躓いた点をまとめました。

# 忙しい人向けのまとめ
* `app.json`（もしくは`app.config.js`）の`expo.ios.infoPlist.WKAppBoundDomains`にService Workerを使用したいサイトのFQDNを記載する
* `expo build`や`expo publish`でビルドしたアプリをインストールすることでService Workerを使用することができる
* `expo start`を使用してExpo Go経由でインストールしたアプリでは`infoPlist`の設定は反映されない

# iOSにおけるWebView
大前提として、iOSのWebViewにはWKWebViewとSFSafariViewControllerがあり、Service Workerを使用できるバージョンやカスタマイズできるポイントが異なります。
（UIWebViewは非推奨なので除外してます）。

|| WKWebView | SFSafariViewController |
|:-:|:-:|:-:|
|Service Worker|iOS 14.0から使用可能|iOS 11.3から使用可能|
|見た目のカスタマイズ|自由にカスタマイズ可能|一部しか変更できない|
|Expo上でのパッケージ|react-native-webview|expo-web-browser|

### Service Worker
iOSのSafariではiOS 11.3からService Workerがサポートされていますが、WKWebViewではiOS 14.0からでないと使用できず、使用するための設定が必要になります（この後説明します）。
対してSFSafariViewControllerでは、基本的にSafariと同等のAPIを使用することができるため、iOS 11.3からService Workerを使用することができます。こちらは特に設定などは必要なく、デフォルトでService Workerを使用することができます。

### 見た目のカスタマイズ
WKWebViewは1つのコンポーネントとして自由にカスタマイズできますが、代わりにブラウザバックボタンなどは自前で実装する必要があります。
SFSafariViewControllerは画面いっぱいにオーバーレイされるように表示されます。ブラウザバック／フォワードボタンや更新ボタンなどは標準で実装されていますが、カスタマイズ出来る部分が限られています。
Twitterのアプリ内ブラウザはSFSafariViewControllerで実装されているらしく、あのイメージです。

### 今回のアプリでは
iOSは14.0以降がサポート対象、かつWebViewの見た目を細かくカスタマイズしたいという要件があったため、WKWebView（react-native-webview）を採用する運びとなりました。

# WKWebViewではデフォルトでService Workerが使用できない
WKWebViewではiOS 14.0からService Workerが使用可能と書きましたが、デフォルトでは使用できず、Service Workerを使用するための設定が必要となります。
この設定を行うファイルが、iOSアプリ開発で出てくる`info.plist`です。
詳しくは説明しません（というかわからない）が、`info.plist`はiOSアプリにおける設定ファイルです。（Androidアプリでいうところの`AndroidManifest.xml`と同じような位置付けだと理解してます。）

WKWebViewでService Workerを使用するには、`info.plist`に`WKAppBoundDomains`を記載する必要があります。

# WKAppBoundDomains
簡単に言うと、WKWebViewでブラウジングするときに信頼できるサイトのドメインリストです。

`WKAppBoundDomains`でサイトのドメインを指定して、その中であればService Worker並びにブラウザの強力なAPIが使えるよ、というものです。
最大10個まで指定することができて、指定したいサイトのFQDNを記載することで機能します（スキーム付きで指定していて機能しなくてハマった）。
例：`https://hogehoge.fugafuga.com` の場合`hogehoge.fugafuga.com`を設定する

# なぜそんな設定が必要なのか
[WebKit](https://webkit.org/blog/10882/app-bound-domains/)の公式ドキュメントによると

>多くのアプリケーションは、アプリ内ブラウジングと呼ばれる、ユーザがアプリを離れることなくウェブサイトを表示する便利な方法として、WKWebViewを使用しています。これは素晴らしいユーザー体験を提供しますが、WKWebViewを使用する開発者が利用できる強力な機能により、ホスティングアプリはアプリ内で訪問するすべてのサイトにわたってユーザーを監視することができます。
>
>JavaScript インジェクション、イベントハンドラ、その他の API などの強力な WKWebView 機能は、アプリケーションまたはユーティリティフレームワークによって、ユーザーに関する個人情報を収集および集計しようとする既知のトラッカーと通信するための侵入的な方法で使用されることがあります。これらの手法により、ユーザーがどの画像で一時停止したか、どのコンテンツをコピー／ペーストしたか、スクロール中にページのどのセクションに到達したかが明らかになる可能性があります。
>
>iOS 14.0とiPadOS 14.0では、開発者がユーザーをトラッキングリスクにさらすことなく、アプリ内ブラウジング体験を提供し続けることができるようにしたいと考えています。本日、私たちはApp-Bound Domainsを発表します。これは、オプトインの新しいWKWebView技術で、ユーザーにより高いプライバシーを提供することによってアプリ内ブラウジングを改善するものです。

（機械翻訳しただけなので読みにくくても許して）
とのことです。

色々書いてありますが、要は「WKWebViewでブラウザの機能をフルを使えるとセキュリティ的に良くないので、アプリ側で信頼できるドメインを指定して、そこでなら使えるようにしよう」ということです。

# Expo側でWKAppBoundDomainsを指定する
`WKAppBoundDomains`を設定すればいいことはわかりましたが、Expoプロジェクト内には`info.plist`は存在しません（ベアワークフローを除く）。
代わりに、Expoアプリの設定ファイルである`app.json`上に`info.plist`相当の設定を記載することができます。
[Expoの公式ドキュメント](https://docs.expo.dev/versions/latest/config/app/#infoplist)によると、`expo.ios.infoPlist`が`info.plist`に相当するようです。
`app.json`の`expo.ios.infoPlist.WKAppBoundDomains`にService Workerを使用したいサイトのFQDNを記載します。
`https://hogehoge.fugafuga.com` というサイトの場合は、下記のようになります。
```app.json
{
  "expo": {
    ...
    "ios": {
      ...
      "infoPlist": {
        "WKAppBoundDomains": [
          "hogehoge.fugafuga.com"
        ]
      }
    }
  }
}
```

また、WebViewの設定で[`limitsNavigationsToAppBoundDomains`](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#limitsnavigationstoappbounddomains)を`true`にすることで、`WKAppBoundDomains`に設定されたドメインのみにアクセスを制限することもできます。
```tsx
<WebView limitsNavigationsToAppBoundDomains={true} />
```

ちなみに[`infoPlist`](https://docs.expo.dev/versions/latest/config/app/#infoplist)の項目に
>(object) - スタンドアロン アプリのネイティブ Info.plist に追加する任意の設定の辞書です。他のすべてのExpo固有の設定より前に適用されます。他の検証は行われないので、**App Store から拒否されるリスクはありますが、ご自身の責任で使用してください。**

という恐怖の一文があります。App Storeへ提出する際にリジェクトを食らう可能性があると言っていますが、この方法で`WKAppBoundDomains`を設定したアプリがApp Storeの審査を通過しているので問題はないはずです。

# いざビルド
`expo start`でアプリをビルドしてみるもService Workerが動きません。
設定を間違えたか？と思い見直してみるも間違っているところはない模様。

# `expo start`では`infoPlist`の設定が反映されない
調べた結果、`expo start`でビルドしたアプリでは`infoPlist`の設定が反映されないようです。

公式ドキュメント上からはそのような記載を見つけられなかったのですが、よくよく[`infoPlist`](https://docs.expo.dev/versions/latest/config/app/#infoplist)の項目を見てみると
> (object) - **スタンドアロン アプリ**のネイティブ Info.plist に追加する任意の設定の辞書です。他のすべてのExpo固有の設定より前に適用されます。他の検証は行われないので、App Store から拒否されるリスクはありますが、ご自身の責任で使用してください。

確かに**スタンドアロンアプリの**と書いてあります。

# スタンドアロンビルドする
Expoでスタンドアロンビルドするには
* `expo build`
* `expo publish`

を使用します。
`expo start`は簡易ビルドのようなもので、ビルドしたアプリは「Expo Go」というExpoクライアントアプリ上で動作します。ホットリロードが可能だったりする代わりに、上述したような一部の機能が制限されます。
対して`expo build`では、実際にスマホにインストールするのと近い形式でアプリをビルドすることができます。その代わり一回のビルドにはそこそこ時間がかかります（ビルドの待機時間にもよりますが、Expoサーバ上でビルドする場合は15~30分程度かかりました）。

スタンドアロンビルドしたアプリでは問題なくService Workerが動作することを確認できました。