JOURNAL

スマホのブラウザでのファイルダウンロードの挙動の違い

「保存ボタンを押したはずなのに、ファイルがどこにも見当たらない……!」

スマホでWebサイトからファイルをダウンロードしたとき、この「異次元へのロスト体験」をしたことがある人はかなり多いのではないでしょうか。PCなら「ダウンロードフォルダ」に直行するだけの単純な話なのに、スマホ(特にiOSとAndroid、さらにアプリ内ブラウザ)が絡むと、ダウンロードの挙動は一気にカオスと化します。

赤猫座の道具(モールス信号変換ツールでのHTML書き出しや、各種テキスト保存機能など)を作るにあたっても、この「スマホごとのダウンロード癖」には散々振り回されました。今回はその実態とフロントエンドでの対処法をまとめてみます。

1. iOS vs Android vs アプリ内ブラウザの3大カオス

同じ「ファイルを保存する」というリンクを踏んでも、閲覧している環境によって起きる現象が全く違います。安全性を最優先にしているからこその問題だという事はわかっているのですが、マジで何かしてほしい問題です…。😿

  • iOS Safari(画面で開いて満足しがち):
    HTMLやテキスト、PDFなどをダウンロードさせようとすると、保存ダイアログを出さずに「新規タブでそのままプレビュー表示して終了」することがよくあります。また、ダウンロードされた場合もアドレスバーの小さな丸矢印アイコン(↓)に格納されるため、初心者は「何も起きなかった」と勘違いしがちです。
  • Android Chrome(比較的素直だが拡張子事故):
    素直に端末の「ダウンロード」フォルダに保存してくれますが、MIMEタイプ(データの種類)の指定が少しでもズレていると、拡張子が勝手に書き換わったりdownloadfile.binという謎ファイルで保存されたりします。
  • アプリ内ブラウザ(LINE、X / Twitter、Instagramなど):
    最凶のラスボス👿アプリ内のセキュリティ制限により、JavaScriptによるファイルダウンロード(Blob URLの保存)が完全に無効化・ブロックされているケースが多々あります。ボタンを押してもウンともスンとも言いません。

2. JavaScriptでファイルを生成して保存させる定番手法

ブラウザ完結型のWebツールで、ユーザーが作成したデータをその場でファイル化してダウンロードさせる場合、一般的には以下のようなBlobオブジェクトを使った手法をとります。

// ブラウザ上でファイルを動的生成してダウンロードさせるコード
function downloadFile(content, filename, mimeType) {
  // 1. データをBlob(バイナリの塊)に変換
  const blob = new Blob([content], { type: mimeType });
  
  // 2. ブラウザ内の一時URL(blob:...)を発行
  const url = URL.createObjectURL(blob);
  
  // 3. 見えないaタグを作ってクリックを偽装
  const a = document.createElement('a');
  a.href = url;
  a.download = filename; // 保存ファイル名を指定
  document.body.appendChild(a);
  a.click();
  
  // 4. 後片付け
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

このa.download属性を使ったコードはPCブラウザなら100点満点で動きますが、スマホ環境では以下のトラップが待ち構えています。

⚠️ 非同期処理のあとに a.click() を呼ぶとSafariで無視される罠

ユーザーのタップ直後ではなく、fetch() や重い計算処理が終わったコールバック内で a.click() を実行すると、iOS Safariのセキュリティ機能(ポップアップブロック)が「ユーザーの直接操作ではない」と判断してダウンロードを無言で握りつぶします。

3. スマホ時代の救世主:Web Share APIという選択肢

「無理にダウンロードリンクを踏ませるからOSの壁にぶつかるのでは?」という発想から生まれたのが、スマホネイティブの共有メニューを呼び出すWeb Share API(navigator.share)です。

// Web Share APIでスマホの共有シートを開く
async function shareFile(file) {
  if (navigator.canShare && navigator.canShare({ files: [file] })) {
    try {
      await navigator.share({
        files: [file],
        title: '作成したファイル',
        text: '赤猫座の道具で作成したファイルです。',
      });
    } catch (err) {
      if (err.name !== 'AbortError') console.error(err);
    }
  } else {
    // 未対応ブラウザは従来のダウンロード処理へフォールバック
  }
}

これを使うと、スマホおなじみの「AirDropで送る」「ファイルアプリに保存」「LINEで共有」「Googleドライブにアップ」といった選択肢がOS標準のUIで下からフワッと出てきます。ユーザーが自分で保存先を選べるため、「どこに消えたか分からない問題」を根本から解決できる超強力な武器です。

閲覧環境従来のBlobダウンロードWeb Share API(共有)推奨されるUX対応
PC(Chrome/Safari/Edge)◎(指定フォルダに直行)△(PCは非対応ブラウザ多め)従来のBlobダウンロードが最適
iOS Safari(iPhone/iPad)△(新規タブで開く等ブレあり)◎(ファイル保存・AirDrop等)Web Share APIを優先適用
Android Chrome○(ダウンロードフォルダへ)◎(Driveや他アプリへ連携)どちらでも快適に動作
アプリ内ブラウザ(LINE等)×(完全にブロックされる)×(共有も動かないことあり)「外部ブラウザで開く」を案内

結論:保存できた「その先」まで案内するのが道具の優しさ

どれだけ技術的に工夫しても、スマホのダウンロード挙動を100%統一することは不可能です。

だからこそ大事なのは、保存ボタンを押した直後に「ファイルを保存しました(※iPhoneの方は画面右上の↓アイコンまたはファイルアプリをご確認ください)」といった案内トーストやメッセージを添えてあげること。技術の限界をユーザーへの優しいマイクロコピーで埋める、これもまた道具作りで学んだ大切な教訓の一つです。