Как проверить фоновую отправку файлов через URLSession

Как проверить фоновую отправку файлов через URLSession

При работе на удалённой машине легко сделать неверный вывод: если файл успешно отправился, пока приложение открыто в симуляторе iOS, значит, задача не потеряется и после переключения пользователя на другое приложение. Но фоновая отправка зависит от файла на диске, передачи задачи под управление системы и обратных вызовов при восстановлении приложения. Одного факта отправки запроса недостаточно. Сначала проверим базовый сценарий с локальным приёмником на облачном Mac, а затем перечислим проверки, которые обязательно нужно провести на реальном устройстве.

Сначала определим границы проверки

Здесь проверяется задача отправки файла, созданная через URLSessionConfiguration.background, а не ожидание восстановления сети с объектом Data в памяти. Подготовьте образец файла с различимым размером, не содержащий настоящих учётных данных, и запишите его размер в байтах и SHA-256. Сервер должен как минимум фиксировать метод запроса, число принятых байтов и возвращённый статус. На клиенте отдельно записывайте идентификатор задачи, ошибку передачи и код состояния HTTP.

Если ошибка в didCompleteWithError равна nil, это означает лишь то, что на уровне передачи не сообщалось об ошибке. Сервер всё равно мог вернуть 4xx или 5xx. При проверке обязательно учитывайте HTTP-ответ.

Симулятор подходит для проверки этой базовой цепочки. Но он не заменяет реальное устройство при проверке того, запустит ли система приложение снова в определённых условиях, и не позволяет проверить переключение настоящей мобильной сети.

Запустим простой приёмник на Mac

Сохраните следующий скрипт как receiver.py и запустите его на том же Mac, где работает симулятор. Он прослушивает только петлевой адрес, ограничивает размер одного запроса и записывает полученные данные по фиксированному тестовому пути. Не используйте его как общедоступный сервис для приёма файлов.

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, только в тестовой сборке настройте исключение App Transport Security с областью действия, ограниченной localhost или используемым петлевым узлом. В сборке для выпуска по-прежнему нужен 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() {
        // После обновления состояния приложения вызвать сохранённый системный обработчик завершения
    }
}

Два приватных метода в примере — точки интеграции с приложением, а не готовая реализация завершения отправки. При интеграции сохраняйте связь между задачей и файлом, с которым работает приложение. Когда делегат приложения получает события фоновой сессии, восстановите сессию с тем же идентификатором, снова подключите делегат и сохраните переданный системой обработчик завершения по идентификатору. Вызывайте его только после обработки событий и обновления состояния интерфейса. Не удаляйте ещё не проверенный файл приложения сразу после одного успешного обратного вызова.

Проверяйте поэтапно: принудительное завершение не равно уходу в фон

Сначала оставьте приложение на переднем плане и отправьте один образец файла. После ответа 201 от приёмника сравните SHA-256 файла /tmp/ios-upload-received.bin и исходного файла. Только совпадение подтвердит, что в этом прогоне получено полное ожидаемое содержимое. Затем возьмите другой образец, отправьте задачу и через интерфейс симулятора вернитесь на главный экран. Дождитесь обратного вызова и снова сверьте файлы. Не завершайте процесс принудительно командой, чтобы имитировать обычный переход в фон: принудительное закрытие приложения пользователем и восстановление после передачи задачи системе — разные ситуации.

Этап проверки Что зафиксировать
Создание задачи Идентификаторы сессии и задачи, путь к исходному файлу и число байтов
Приём на сервере HTTP-статус, фактическое число байтов, SHA-256 файла
Завершение на клиенте Ошибку передачи и HTTP-статус по отдельности
Восстановление приложения Восстановление сессии, поступление обратного вызова и порядок вызова обработчика завершения

На реальном устройстве отдельно проверьте блокировку экрана, восстановление после обрыва сети, смену типа сети и повторный запуск приложения после завершения его системой. Принудительное закрытие пользователем вынесите в отдельный тест и не считайте его результат показателем обычной работы в фоне. Если к удалённому Mac нельзя подключить устройство, которое находится у вас под рукой, передайте собранные в симуляторе данные и список оставшихся проверок коллеге с реальным устройством. Не пишите в отчёте «проверено на реальном устройстве».

Типичные сбои и завершение проверки

Если сервер не получил запрос, сначала проверьте, указывает ли URL файла на существующий обычный файл, затем — тестовый адрес, порт и настройки безопасности передачи. Если файл пришёл, но клиент отметил сбой, проверьте статус ответа: пример считает успешным только 201. Отсутствие ошибки передачи не означает, что сервер принял файл. Если после восстановления приложения обратный вызов не поступил, проверьте, восстановлена ли фоновая сессия с прежним идентификатором, удерживается ли объект делегата и не был ли обработчик завершения вызван слишком рано.

В конце остановите тестовый приёмник, удалите /tmp/ios-upload-received.bin и очистите исходные файлы и сетевые исключения, использовавшиеся только для теста. Для рабочего процесса также задайте предел повторных попыток после сбоя и порядок обработки повторных отправок на сервере; удаляйте файлы лишь после подтверждения, что результат сохранён. Тогда следующему инженеру достанется не фраза «фоновая отправка работает», а воспроизводимые шаги, проверяемые данные и список ещё не выполненных проверок на реальном устройстве.

Часто задаваемые вопросы

Можно ли передать Data из памяти через фоновую URLSession?

В этом сценарии используйте задачу с файлом: сначала запишите данные на диск, затем вызовите uploadTask(with:fromFile:).

Успех в симуляторе подтверждает восстановление приложения на устройстве?

Нет. Симулятор подходит для проверки запроса, ответа и базовых обратных вызовов. Повторный запуск системой, смену сети и поведение после принудительного закрытия проверяйте отдельно на устройстве.

Выделенный физический сервер

Подготовьте Mac для следующей удалённой разработки

Выберите модель и регион под свои задачи, а затем подключитесь через общий доступ к экрану, VNC или SSH. Актуальные цены и доступность смотрите в консоли.

Настроить удалённый Mac