リモート Mac で iOS のバックグラウンドアップロードを検証する

リモート Mac で iOS のバックグラウンドアップロードを検証する

リモート開発環境では、iOS シミュレータでアプリを前面にしたままファイルのアップロードに成功すると、ユーザーが別のアプリに切り替えた後もタスクは失われないと思いがちです。しかし、バックグラウンドアップロードにはディスク上のファイル、システムに引き継がれる転送タスク、アプリ復帰時のコールバックが関わります。リクエストが送信されたかどうかだけでは判断できません。まずクラウド上の Mac で動かすローカルの受信サーバーを使って基本動作を検証し、続いて実機で確認すべき項目を整理します。

検証範囲を先に決める

今回検証するのは、URLSessionConfiguration.background で作成したファイルアップロードタスクです。Data をメモリに保持してネットワークの復旧を待つ方式ではありません。サイズを識別でき、実際の認証情報を含まないサンプルファイルを用意し、バイト数と SHA-256 を記録します。サーバー側では少なくともリクエストメソッド、受信バイト数、返したステータスを記録できるようにします。クライアント側ではタスク識別子、転送エラー、HTTP ステータスコードをそれぞれ記録します。

didCompleteWithError のエラーが nil でも、転送レイヤーからエラーが報告されなかったという意味にすぎません。サーバーが 4xx や 5xx を返す可能性はあるため、検証時には HTTP レスポンスも確認する必要があります。

シミュレータは、この基本的な一連の処理を確かめるのに適しています。ただし、特定の条件下でシステムがアプリを再起動することの証明や、実際のモバイルネットワーク切り替えの検証には実機が必要です。

Mac で最小構成の受信サーバーを起動する

次のスクリプトを receiver.py として保存し、シミュレータを動かしている Mac で実行します。ループバックアドレスだけで待ち受け、1 回のリクエストサイズを制限し、受信内容を固定のテスト用パスに書き込みます。インターネットに公開するアップロードサービスとしては使用しないでください。

from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path

LIMIT = 10 * 1024 * 1024
OUTPUT = Path("/tmp/ios-upload-received.bin")

class Receiver(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        if self.path != "/upload" or not 0 < length <= LIMIT:
            self.send_error(400)
            return
        body = self.rfile.read(length)
        if len(body) != length:
            self.send_error(400)
            return
        OUTPUT.write_bytes(body)
        self.send_response(201)
        self.end_headers()

HTTPServer(("127.0.0.1", 8765), Receiver).serve_forever()

起動コマンドは python3 receiver.py です。テストアプリから http://127.0.0.1:8765/upload にアクセスする場合は、テストビルドに限り、localhost または使用するループバックホストに対象を絞った App Transport Security の例外を設定します。リリースビルドでは引き続き HTTPS を使用してください。シミュレータから接続できなければ、まずサーバープロセスが動いていることとポートが一致していることを確認し、次に例外設定を調べます。最初からアップロード処理を変更しないでください。

ディスク上のファイルからバックグラウンドタスクを作成する

次の例は、タスク作成の要点を示します。呼び出し側は、書き込みが完了し、現在のプロセスから読み取れるファイルの URL を渡します。endpoint はテスト設定から与え、ローカルのテスト用アドレスをリリース用コードに埋め込まないでください。バックグラウンドセッションの識別子はこのアップロード処理に固有の固定値とし、起動のたびに新しい識別子を生成しないようにします。

import Foundation

final class UploadCoordinator: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(
            withIdentifier: "com.example.upload-check"
        )
        config.isDiscretionary = false
        config.sessionSendsLaunchEvents = true
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    func upload(file: URL, endpoint: URL) {
        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        session.uploadTask(with: request, fromFile: file).resume()
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: Error?
    ) {
        let status = (task.response as? HTTPURLResponse)?.statusCode
        let accepted = error == nil && status == 201
        recordResult(taskID: task.taskIdentifier, accepted: accepted)
    }

    func urlSessionDidFinishEvents(
        forBackgroundURLSession session: URLSession
    ) {
        finishPendingBackgroundEvents()
    }

    private func recordResult(taskID: Int, accepted: Bool) {
        // 結果をアプリ側の永続化する状態記録機構に渡す
    }

    private func finishPendingBackgroundEvents() {
        // アプリの状態更新後、保存しておいたシステムの完了ハンドラを呼び出す
    }
}

例にある 2 つの private メソッドはアプリに組み込むための接続点であり、アップロード完了処理の実装ではありません。実際に組み込む際は、永続化する結果にタスクと業務上のファイルとの対応関係を残します。アプリデリゲートがバックグラウンドセッションのイベントを受け取ったら、同じ識別子でセッションを再作成してデリゲートを接続し、システムから渡された完了ハンドラを識別子ごとに保存します。イベントの処理と画面の状態更新が済んでから、そのハンドラを呼び出してください。成功コールバックが 1 件届いただけで、未確認の業務用ファイルをすぐに削除してはいけません。

段階的に検証し、強制終了をバックグラウンド動作と混同しない

まずアプリを前面に保ったまま、サンプルファイルを 1 件送信します。受信サーバーが 201 を返したら、/tmp/ios-upload-received.bin と元のファイルの SHA-256 を比較します。両者が一致して初めて、今回受信した内容が期待どおりに完全であると確認できます。次に別のサンプルを使い、タスクを送信した後、シミュレータの画面操作でホーム画面に戻ります。コールバックを待ち、ファイルを再度照合します。通常の「バックグラウンドへの切り替え」を模擬するために、コマンドでプロセスを強制終了しないでください。ユーザーによる強制終了と、システムがタスクを引き継いだ後のアプリ復帰は別の状況です。

確認項目 記録する証拠
タスク作成 セッション識別子、タスク識別子、元ファイルのパスとバイト数
サーバーでの受信 HTTP ステータス、実際のバイト数、ファイルの SHA-256
クライアント側の完了 転送エラーと HTTP ステータスを別々に記録
アプリの復帰 セッションの再作成、コールバックの到着、完了ハンドラ呼び出しの順序

実機での検証では、画面ロック、ネットワーク切断後の復旧、ネットワーク種別の切り替え、システムによるアプリ終了後の再起動も個別に確認します。ユーザーによる強制終了は別のテストとして扱い、その結果を通常のバックグラウンド動作の結果とみなさないでください。リモート Mac から手元の実機に接続できない場合は、まずシミュレータで得た証拠と未実施のテスト項目を実機を持つ同僚に渡します。報告書に「実機で確認済み」と書いてはいけません。

よくある失敗と後片付け

サーバーにリクエストが届かない場合は、まずファイル URL が実在する通常のファイルを指しているか確認し、次にテスト用アドレス、ポート、通信セキュリティの設定を調べます。ファイルは届いているのにクライアントが失敗と判定する場合は、レスポンスのステータスを確認してください。この例では 201 だけを成功とみなします。「転送エラーがない」ことを「サーバーが受け付けた」ことと混同してはいけません。アプリ復帰後にコールバックが届かない場合は、元の識別子でバックグラウンドセッションを再作成したか、デリゲートオブジェクトが保持されているか、完了ハンドラを早すぎる段階で呼び出していないかを確認します。

最後にテスト用の受信サーバーを停止し、/tmp/ios-upload-received.bin を削除して、テスト専用の元ファイルとネットワーク例外も片付けます。本番の処理ではさらに、失敗時の再試行回数の上限と重複送信に対するサーバー側の扱いを定め、結果が永続化されたことを確認してからファイルを削除する必要があります。次のエンジニアに引き継ぐべきものは「バックグラウンドアップロードは動く」という一言ではなく、再実行できる手順、照合できる証拠、そして未実施の実機テスト項目です。

よくある質問

バックグラウンド URLSession でメモリ上の Data を直接送信できますか?

この手順ではファイルを送信元にします。データを先にディスクへ書き出し、uploadTask(with:fromFile:) でタスクを作成してください。

シミュレータで成功すれば、実機でのアプリ復帰も確認済みですか?

いいえ。要求と応答、基本的なコールバックはシミュレータで調べられますが、システムによる再起動や通信切替、強制終了後の動作は実機で確認します。

専有物理サーバー

次回のリモート開発に備えてMacを用意

用途に合わせてモデルとロケーションを選び、画面共有、VNC、SSHで接続できます。価格と空き状況はコンソールでリアルタイムにご確認ください。

リモートMacを設定する