При работе на удалённой машине легко сделать неверный вывод: если файл успешно отправился, пока приложение открыто в симуляторе 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. Актуальные цены и доступность смотрите в консоли.