🫠

iOS Safari(特にWebView)のアプリ復帰はvisibilitychangeだけでは判定しきれず悩まされた

に公開

問題

visibilitychangeで可視状態を判定していたが、iOS Safari(特に WebView)で、アプリ復帰時に可視状態の更新が遅れる/連動しないケースがあった

原因

ブラウザ仕様 × Safari/WKWebView の実装差により、アプリ復帰時の focusvisibilitychange の発火のタイミングが前後する。

解決策

早く届いた信号で楽観的に更新し、後から届いた信号で上書き確定する。

  • 早期:focus(復帰の早い合図)
  • 確定:visibilitychange(visible/hidden)(最終整合)

代表的なシーケンス
アプリ復帰:blur →(BG)→ focusvisibilitychange(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