iOSの不完全なPWAと戦った記録


みなさん、PWAは活用されていますか?苦しめられていませんか?

PWA( Progressive Web Apps )は、ブラウザで動作するWebアプリケーションとネイティブアプリケーションの両方の特徴を兼ね揃えたアプリケーションです。ホーム画面にインストールすればすぐにアクセス出来るようになり、オフラインで一部機能が使えたり、プッシュ通知を受け取れたりと大変便利な技術です。

開発側の視点で言えば、通常のWebアプリのノウハウで開発が可能で、かつストアへの登録が不要という手軽さも手伝って非常に多くのWebアプリがPWAに対応しています。

Gaji-Labo がプロダクト開発を支援しているスタートアップにおいても、多くの場面でPWAは活躍しています。やはりWebアプリケーションのコードをほぼそのまま活用出来る点は大きな魅力であり、ユーザーの使い勝手向上はもちろん、さらにはアプリケーション自体の価値向上を図るために非常に頼もしい存在です。

iOSのPWA対応の遅れ

しかし、iOSのPWAへの対応状況は芳しくありません。

元々Web標準への追随が遅れがちなiOSではありますが、それはPWAにおいても勿論同様で、PWAの最大のメリットとも言えるプッシュ通知にようやく対応したのが2023年リリースされた iOS 16.4 でした。執筆当時から見て「昨年」です。

そして今なお、iOSのPWA対応はAndroidと比較すると多くの点で不完全です。わたしたち開発者はどこが完全でないのかを知り、そして戦わなければなりません。

この記事ではiOSでのPWA実装で陥りやすい落とし穴の共有と、その対応策について考えていきます。

iOSがPWAでできないこと、その対応策

browser モードでプッシュ通知が使えない

PWAは display という設定によりホーム画面に追加した時の挙動を制御できます。 fullscreen standalone minimal-ui browser の中から選択するのですが、 browser を設定した場合はブックマークとして機能し、ホーム画面に設置されたアイコンをタップするとブラウザーで開かれます。

{
  "$schema": "https://json.schemastore.org/web-manifest-combined.json",
  "name": "Awesome Web App",
  "short_name": "Awesome Web App",
  "start_url": ".",
  "display": "browser",
  ...
}

さて、iOSがプッシュ通知に対応したと先ほど申し上げましたが、プッシュ通知を利用するためには standalone (あるいはfullscreen )でなければなりません。Safariではプッシュ通知に対応していないため、 browser では利用できないのです。

対策としては単純に standalone を選択すれば良いのですが、そこで新たな問題が発生します。ナビゲーションが使えない問題です。

standalone モードでナビゲーションが使えない

スマートフォンのブラウザはジェスチャナビゲーションでコントロールができます。例えば…

  • 下に引っ張るとページをリフレッシュする(Pull to refresh)
  • 左端から右方向にスワイプすると「戻る」
  • 右端から左方向にスワイプすると「進む」

環境によっては他にも使える機能があるかもしれませんが、広く知られているのはこの3つですね。iOSのSafariでもこれらのジェスチャを使って快適にWebブラウジングすることができます。

しかし残念なことに、 現在のiOSの standalone のPWAでは、ジェスチャナビゲーションが使えません。加えて、 standalone の画面はフルスクリーンビューなので通常のブラウザに備わっている「戻る」「進む」「更新」といったナビゲーションUIがありません。つまりページ内に実装されているUIを使う以外にページ遷移をする手段がないので、例えばアプリを使っていて予期せずエラー画面になってしまった場合、その画面に「ホームに戻る」ボタンなどが用意されていなければ立ち往生してしまうというわけです。

この問題は browser モードを使う事で解消できるのですが、そうすると今度はプッシュ通知が利用出来なくなるという堂々巡りに陥ります。

結論としては、PWAを活用する上でプッシュ通知はなくてはならない存在なので、 standalone モードで起動させて、PWAで開かれている場合にはブラウザのUIを模したナビゲーションUIをHTMLで実装する、というあたりが現実的な落とし所ではないでしょうか。

共有機能が使えない

ナビゲーションが使えない話と関連ではありますが、Safari のナビゲーションUIにある「シェア」の機能もまた standalone では使えなくなります。

Webアプリである以上、URLをシェアしたいというニーズは当然あります。これも「戻る」ボタンなどと同様に自前で実装してやると良いでしょう。シェアの機能については専用のAPIが標準で備わっているので、これを利用すれば実現は容易です。

localStorage を Safari と共有できない

iOSのPWAは、Safariと localStorage のデータを共有できません。例えば認証状態を localStorage で保持する実装ではログイン情報を Safari から PWA に受け渡すことができないため、PWAで開いた際に再度ログインする必要があります。

試してみたところ、意外なことに Cookie は共有する事ができました。SafariからPWAへデータを受け渡ししたい場合は Cookie を使った実装が有用そうです。

ちなみに sessionStorage はもちろん別セッション扱いとなるため使えませんでしたが、これはAndroidも同様でした。

iOSのPWA対応に役立つかもしれないTips

上で挙げたものの他にも iOS の PWA は色々とクセがあり扱いづらいです。PWAと上手に付き合っていくためのTipsをいくつかここで紹介します。

最初にロードされた manifest.json が活かされる

ご存知のように、PWAを実装する上で欠かせないのが manifest.json です。このファイルに対してHTMLの <link /> 要素でリンクをはっておくことで、PWAとして登録出来るようになります。

<link rel="manifest" href="manifest.json" />

iOS Safari では manifest.json最初にロードされたものが優先されます。あとから href を変更しても、変更前の manifest.json が活かされます。「どういうこと?」と思われるかもしれないので、コードで説明します。

<link rel="manifest" href="manifest.json" />
<script>
{
  if (isCondition) {
    const $link = document.querySelector("link[rel=manifest]");
    $link.setAttribute("href", "manifest2.json");
  }
}
</script>

このような実装で、Androidでは manifest2.json が活かされますが、一方 iOS では最初にロードされた manifest.json のまま変わりません。このことでどのように困るかというと、例えば「特定の条件下では別の manifest2.json を参照させたい」という要件があった場合にこの挙動を知らなければ貴重な時間をたっぷり溶かすことになるでしょう。

これは「最初にロードされたもの」というところが肝なので、「最初にロード」させなければよいのです。URLの差し替えではなく <link /> 要素の挿入であればうまくワークします。

{
  const $link = document.createElement("link");
  $link.setAttribute("rel", "manifest");
  $link.setAttribute("href", isCondition ? "manifest2.json" : "manifest.json");
  document.head.appendChild($link);
}

この挿入が1つ目の manifest でなければならないので、これ以前に <link rel="manifest" /> は記述されていてはいけません。

PWA環境の判別方法

このようにクセのある環境ですから「iOSのPWAでのみ別の処理をさせたい」という場面もあるでしょう。そこで「PWA環境で実行されている」ことを判別する方法が知りたくなります。レガシーブラウザ対応を彷彿とさせますね。

PWA環境下であることを知るためにはCSSのメディアクエリが有用です。メディアクエリで display-modestandalone を指定することで、PWAで開かれているかどうかを判別することができます。

@media all and (display-mode: standalone) {
  /* PWA向けのスタイル */
}

JavaScriptで判別したい場合も同様にメディアクエリを使います。

const isPWA = window.matchMedia("(display-mode: standalone)").matches;

ここで注意しなければならない点は、 fullscreen では駄目だということです。なぜなら、PCのブラウザで全画面表示をした場合も fullscreen になってしまうからです。

さらに、 standalone で検知するためには manifest.jsondisplayfullscreen ではなく standalone にしなければなりません。 見た目が変わらないからといって fullscreen にしてはいけないのです。判別したければ、PWAでは必ず displaystandalone を指定しましょう。

おわりに

Webの標準化が進み、レガシーブラウザ対応のPolyfillやCSSハックなど、特定環境で役立つ知識や技術の多くは過去のものとなりました。しかし、今なお端末の環境は多岐にわたり、それらをサポートするために開発者は様々な知見を常に蓄えていかなければなりません。

これは今も昔も変わらず、Web技術が進化していくうえで欠かすことのできない流れです。iOSのPWAもいずれWeb標準に則るようになり、これらの知識は不要になるでしょう。しかし、その時はまた新たな課題に挑戦していることと思います。

Gaji-Labo では毎週「テックシェア」という時間を設け、メンバーそれぞれが案件などで得た知見を共有したり、質問・相談をし合う場として活用しています。「テックシェア」を通して知識は組織内で横展開され、Gaji-Labo が支援しているプロダクトでそれぞれ発揮されていきます。ひとりが得た知見はメンバー全員が得た知見として Gaji-Labo というチームの強さを支え、結果として開発支援するスタートアップの価値を高めていくのです。

Gaji-Labo は新規事業やサービス開発に取り組む、事業会社・スタートアップへの支援を行っています。

弊社では、Next.js を用いた Web アプリケーションのフロントエンド開発をリードするフロントエンドエンジニアを募集しています!さまざまなプロダクトやチームに関わりながら、一緒に成長を体験しませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください!

求人応募してみる!


投稿者 Oikawa Hisashi

フロントエンドエンジニア。モダンなJavaScript開発に関心があります。 デザインからバックエンドまで網羅的にこなすマルチデザイナーとして長く活動してきた経験を活かして、これから関わる様々なものをデザインしていきたいです。チームもコミュニケーションもデザインするもの。ライフワークはピアノと水泳。