🫠
iOS Safari(特にWebView)のアプリ復帰はvisibilitychangeだけでは判定しきれず悩まされた
問題
visibilitychangeで可視状態を判定していたが、iOS Safari(特に WebView)で、アプリ復帰時に可視状態の更新が遅れる/連動しないケースがあった
原因
ブラウザ仕様 × Safari/WKWebView の実装差により、アプリ復帰時の focus と visibilitychange の発火のタイミングが前後する。
解決策
早く届いた信号で楽観的に更新し、後から届いた信号で上書き確定する。
- 早期:
focus(復帰の早い合図) - 確定:
visibilitychange(visible/hidden)(最終整合)
代表的なシーケンス
アプリ復帰:blur→(BG)→focus→visibilitychange(visible)
| イベント | 発火対象 | 何を示す? | 主な発火タイミング |
|---|---|---|---|
visibilitychange |
document |
ページの可視状態 | タブ切替、別アプリへ移動、画面ロック 等 |
focus |
window |
ブラウジングコンテキストにフォーカスが当たった | アプリ復帰、タブ選択、ウィンドウがアクティブ |
blur |
window |
フォーカスを失った | 別タブへ移動、別アプリへ切替、ダイアログ表示 |
実装コード
'use client';
import { useEffect, useState } from 'react';
/**
* アプリ復帰時の可視状態を“早期→確定”で安定化するフック。
* - 早期: focus / 非表示: blur
* - 確定: visibilitychange(最後に届いたシグナルで上書き)
*/
export const useAppVisibility = () => {
// 初期は暫定 true にして、マウント後に同期(SSR の話題はスコープ外)
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const syncFromVisibility = () =>
setIsVisible(document.visibilityState === 'visible');
const setTrue = () => setIsVisible(true);
const setFalse = () => setIsVisible(false);
// 初回同期
syncFromVisibility();
// 早期反応(復帰/離脱)
window.addEventListener('focus', setTrue, { passive: true });
window.addEventListener('blur', setFalse, { passive: true });
// 最終確定
document.addEventListener('visibilitychange', syncFromVisibility, { passive: true });
return () => {
window.removeEventListener('focus', setTrue);
window.removeEventListener('blur', setFalse);
document.removeEventListener('visibilitychange', syncFromVisibility);
};
}, []);
return isVisible;
};
一旦、これで検知できるようになった。
Discussion