1. はじめに(完璧に思えた「プレビュー作戦」と新たな絶望)
Webアプリ(PWA)において、「画面内の特定のDOMを画像化してOSのシェアメニューで共有する」という機能を作ろうとした際、これまで数々の深い沼にハマってきました。
前回の記事(※【PWA/iOS】SafariでDOM画像化が「真っ白・見切れる」バグへの最終回答)では、最大の障壁であった「Safariで画像が真っ白になるバグ」について紹介しました。
当時の私は、このSafariでの白抜けバグに対して、苦肉の策として以下のような「プレビュー作戦」を考えていました。
「画面外だから描画をサボるなら、ユーザーの目の前にHTMLとして堂々と表示させれば確実に描画されるはずだ!」という、力技のアプローチです。
【完璧に思えたフロー】
- ユーザーが「決定」ボタンを押す
- プレビューモーダルが開き、HTMLが画面中央にバッチリ描画される(これでSafariの白抜けを回避)
- ユーザーが「共有する」ボタンをタップする
- その瞬間に、画面上のHTMLをキャプチャして画像化し、
navigator.shareでOSのシェアメニューに渡す
完璧な作戦だと思いました。しかし、いざ実機で「共有する」をタップすると、シェアメニューは開かず、コンソールに無慈悲なエラーが出現したのです。
NotAllowedError: The request is not allowed by the user agent or the platform in the current context.
2. なぜエラーが起きるのか?(Webブラウザのシビアなセキュリティ制約)
コードの記述ミスではなく、これはWebブラウザの厳格なセキュリティ仕様によるものでした。
Transient user activation(一時的なユーザーのアクティブ化)の壁
navigator.share のような強力なOSネイティブ機能をWebから呼び出すには、「ユーザーが直接ボタンをクリック・タップした直後である」必要があります。これは、悪意のあるスパムサイトなどが勝手にシェア画面やポップアップを連続で開くのを防ぐためのセキュリティルールです。
非同期処理(await)のタイムアウト
ここの「直後」という時間判定が、想像以上にシビアに設定されています。
ボタンを押したハンドラの中で、await html2canvas(...) のような重い画像生成処理を挟むと、処理が終わるまでに数秒の遅延が発生します。
するとSafariは、「ボタンが押されてから時間が経ちすぎている。これはユーザーの意図した直接的なアクションではないな?ブロック!」と判定し、シェア処理を強制終了させてしまうのです。
3. 解決策:「非同期処理ゼロ」の即時シェア(事前生成アーキテクチャ)
この制約を突破するには、「ボタンを押してからいかに高速に画像を作るか」といったコードレベルのチューニングでは限界があります。
必要なのは、UI/UXレイヤーからの設計変更です。
ユーザーがボタンを押してから画像を作るのではなく、「ユーザーがボタンを押す前(プレビュー画面を見ている間)に、裏側で画像を完成させておく」という「事前生成アーキテクチャ」へ変更します。
具体的な実装フロー(Reactの例)
① 状態管理(State)の準備
生成した画像データと、生成中かどうかのフラグを用意します。
const [shareFile, setShareFile] = useState(null);
const [isGenerating, setIsGenerating] = useState(true);
② バックグラウンド生成(useEffect)
プレビューモーダルが開いた直後に、オフスクリーンDOMに対して画像化ライブラリ(html2canvas-pro)を実行します。生成完了後、shareFile にBlob(またはFile)を保存し、ローディング状態を解除します。
useEffect(() => {
const generateImage = async () => {
setIsGenerating(true);
// 画面外のDOMをキャプチャ
const canvas = await html2canvasPro(targetRef.current);
canvas.toBlob((blob) => {
const file = new File([blob], "share-image.png", { type: "image/png" });
setShareFile(file);
setIsGenerating(false);
});
};
generateImage();
}, []);
③ UIの制御と、同期的なAPI呼び出し
isGenerating が true の間は、シェアボタンを disabled にしてタップを防ぎます。
そして、ユーザーがシェアボタンを押した時のハンドラでは、絶対に await を使わず、即座にAPIを呼び出します。
const handleShare = () => {
// 非同期処理を挟まず、事前に作っておいた画像を即座に渡す
if (navigator.share && shareFile) {
navigator.share({
files: [shareFile],
title: 'シェアする画像',
});
}
};
// レンダリング部分
<button onClick={handleShare} disabled={isGenerating}>
{isGenerating ? '準備中...' : '共有する'}
</button>
これで、Webブラウザのシビアなタイムアウト判定を100%回避できるようになります。
4. ワンランク上のUX:プレビュー自体を「画像」にすり替える
事前生成アーキテクチャを採用したことで、さらにワンランク上のUXを提供できるようになります。
裏側で生成が完了した画像(Blob)は、URL.createObjectURL を使って、プレビューモーダル上の <img> タグのソースとしてそのまま表示してしまいましょう。
もともと表示していたHTMLのDOMを隠し、生成された画像にすり替えるのです。
これにより、「プレビュー画面で今見ているものと、実際にX(Twitter)等にシェアされるものが、1ドットの狂いもなく完全に一致する(WYSIWYG)」という、ユーザーにとって最も安心感のあるUXを提供できます。
5. おわりに(ブラウザの「仕様」には逆らわない)
Web Share APIの NotAllowedError は、決してブラウザの不具合ではなく、ユーザーを守るための正当なセキュリティ仕様です。
この厳格な仕様に対して、「いかに非同期処理を高速化してタイムアウトを避けるか」とコードのチューニングで真正面から戦うのではなく、「ユーザーがボタンを押す前にすべてを終わらせておく(事前生成)」という、UXとアーキテクチャの変更こそが最も確実でスマートな解決策でした。
同じようにDOM画像化とWeb Share APIの沼に苦しんでいる方の参考になれば幸いです。
関連する「DOM画像化の沼」
PWAにおいて「DOMを画像化してシェアする」という機能の実装では、本記事のタイムアウト問題以外にも、環境によって様々な罠が潜んでいます。もし別のエラーで行き詰まった場合は、以下の記事も参考にしてみてください。
-
画像生成エンジンがクラッシュする場合:
Tailwind v4などでモダンCSSを使っていると、画像化の瞬間にパースエラーが起きます。
👉 【解決】html2canvasで「Attempting to parse an unsupported color function "oklch"」エラーが出る原因と対策(Tailwind v4) -
iOS Safariで画像が真っ白になる・見切れる場合:
ネイティブ描画(SVG)に依存するライブラリとSafariの相性問題の可能性があります。
👉 【PWA/iOS】SafariでDOM画像化(html-to-image)が「真っ白・見切れる」バグへの最終回答
泥臭いDOMハックを積み上げるのではなく、ライブラリの選定やアーキテクチャ(仕組み)の変更で解決する。今回の機能実装にあたり、次世代IDEである「Google Antigravity」 と壁打ちをしながらこの結論に至るまでの、個人開発のリアルな裏側と教訓をまとめた記事も書いています。よろしければこちらもご覧ください。
👉 【個人開発】「ただの画像シェアボタン」にPWAの闇と美学を見た話(Safariとの死闘編)