同じアプリなのに挙動が違う?iOS PWAの落とし穴
iOS PWAでキーボードを表示した後、フッター下に操作できない余白が出現するバグに遭遇。原因特定までの試行錯誤と、意外なところにあった解決策の記録。
iOS向けのPWAを作っていて、かなり厄介なバグに遭遇しました。
キーボードを表示した後、フッターメニューの下に操作できない余白が出現するという問題です。
ダークモードだと分かりやすいのですが、フッターの下に濃い黒の領域が出ています。この領域はタップしても反応しません。
しかも厄介なことに、同じバージョンのアプリを同じ端末にインストールしたPWAでも、片方だけで発生するという謎の挙動でした。
最初に試したこと
1. CSSのviewport単位を疑う
最初に疑ったのは 100vh の使用箇所でした。iOS Safariでは 100vh がアドレスバーを含んだ高さになるため、100dvh(Dynamic Viewport Height)を使うべきという話があります。
/* NG: iOS Safariでは意図しない高さになることがある */
height: 100vh;
/* OK: 動的にビューポートの高さを追従 */
height: 100dvh;
実際にコードベースを検索すると、いくつか 100vh を使っている箇所がありました。これを 100dvh に変更してみましたが…効果なし。
2. visualViewport APIでの対処
次に試したのは、visualViewport APIを使ってキーボードが閉じたタイミングでスクロール位置をリセットする方法です。
export function useIOSKeyboardFix() {
useEffect(() => {
const isIOSPWA =
"standalone" in window.navigator &&
window.navigator.standalone === true;
if (!isIOSPWA || !window.visualViewport) {
return;
}
const visualViewport = window.visualViewport;
let previousHeight = visualViewport.height;
const handleResize = () => {
const currentHeight = visualViewport.height;
// キーボードが閉じた(ビューポートが大きくなった)場合
if (currentHeight > previousHeight) {
requestAnimationFrame(() => {
window.scrollTo(0, 0);
});
}
previousHeight = currentHeight;
};
visualViewport.addEventListener("resize", handleResize);
return () => visualViewport.removeEventListener("resize", handleResize);
}, []);
}
これも効果なし。
3. safe-area-insetを疑う
iOS PWAではノッチやホームインジケーターを避けるために env(safe-area-inset-*) を使います。これが悪さをしているのかと思い、一時的に無効化してみました。
/* 通常の設定 */
.pb-safe {
padding-bottom: env(safe-area-inset-bottom);
}
/* テスト用に固定値に変更 */
.pb-safe {
padding-bottom: 0;
}
これも効果なし。
転機:新旧PWAの比較
全く検討がつかず、色々試行錯誤している中で、重要な発見がありました。
同じ端末に「かなり前にインストールしたPWA(旧アプリ)」と「最近インストールしたPWA(新アプリ)」があったのですが、旧アプリでは問題が発生しないのです。
両者の見た目の違いを観察してみると:
| 項目 | 旧アプリ | 新アプリ |
|---|---|---|
| ステータスバー | 白背景で独立 | アプリの背景色と一体化 |
| 表示領域 | ステータスバーの下から | 画面の最上部から |
| 問題の余白 | 発生しない | キーボード後に発生 |
この違いはフルスクリーン表示かどうかの違いでした。
原因:apple-mobile-web-app-status-bar-style
Gitの履歴を追ってみると、ある時点で以下の変更が入っていました:
<!-- 変更前 -->
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<!-- 変更後 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
この設定の違いは:
| 値 | 挙動 |
|---|---|
default | ステータスバーは白背景。アプリはその下に表示される |
black-translucent | ステータスバーが透過し、アプリがフルスクリーン表示される |
black-translucent にするとアプリがステータスバー領域まで使えるようになり、見た目がネイティブアプリに近くなります。ただし、この設定とiOSのキーボード処理の組み合わせでビューポートが正しくリセットされないバグが発生していたようです。
解決策
apple-mobile-web-app-status-bar-style を default に戻しました。
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
これで問題は解消しました。
重要な注意点:PWAの再インストールが必要
ここで一つハマりポイントがあります。
このメタタグはPWAのインストール時にしか読み込まれません。
つまり、HTMLを更新してデプロイしても、既にインストール済みのPWAには反映されないのです。修正を反映するには、ユーザーに一度PWAを削除して再インストールしてもらう必要があります。
これが「同じバージョンなのに片方だけ問題が発生する」という謎の原因でした。旧アプリは default の時代にインストールされたもの、新アプリは black-translucent の時代にインストールされたものだったのです。
学んだこと
1. PWAのメタタグはインストール時に固定される
apple-mobile-web-app-* 系のメタタグは、PWAをホーム画面に追加した時点の値がキャッシュされます。後から変更してもインストール済みのPWAには反映されません。
2. フルスクリーンPWAはiOSで罠が多い
black-translucent を使ったフルスクリーン表示は見た目は良いのですが、iOSのキーボード周りで予期せぬバグを踏む可能性があります。safe-area対応も必要になるため、複雑さが増します。
3. 新旧の比較が原因特定の鍵
今回は「旧アプリでは発生しない」という事実が原因特定の決め手になりました。再現する環境としない環境の差分を見つけることが、デバッグの基本だと改めて感じました。
おわりに
今回の問題は、CSS周りを色々いじっても解決せず、最終的にHTMLのメタタグという意外な場所に原因がありました。
black-translucent を使いたい場合は、キーボードを使う画面で問題が発生しないか十分にテストすることをおすすめします。特にiOSのPWAはSafariのWebViewベースで動いているため、ブラウザでは発生しない問題がPWAでだけ発生することがあります。