Site icon AppTractor

Фоновая загрузка файлов на сервер в iOS

Я заметил, что в сети есть тонны статей с советами и туманными теориями, но почти нет реальных примеров того, как реализовать фоновую загрузку в iOS-приложениях. Поэтому в этой статье я расскажу об основах выполнения кода в фоновом режиме и покажу вам реальный проект, в котором я реализовал фоновую загрузку файлов. Давайте начнем.

Введение

При создании приложений для iOS одной из распространенных задач является обеспечение возможности продолжения выполнения задач в фоновом режиме после выхода пользователя из приложения. Когда приложение переходит в приостановленное состояние (suspended state), система прекращает выполнение кода, чтобы сэкономить ресурсы. По сути, приложение «засыпает».

Однако хорошая новость заключается в том, что с помощью правильных методов и настроек вы можете расширить или включить фоновое выполнение для таких важных задач, как:

Жизненный цикл приложения

Прежде чем перейти к реальным реализациям, давайте быстро разберемся с жизненным циклом приложения и состояниями, через которые оно проходит.

Состояния приложения

1. Активное состояние (Active)

Приложение работает на переднем плане и активно выполняет код. Это состояние по умолчанию, в котором все работает без ограничений.

2. Неактивное состояние (Inactive)

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

3. Фоновое состояние (Background)

Приложение больше не отображается на экране, но система дает ему ограниченное количество времени (около 5 секунд) для выполнения важных задач, например сохранения данных или завершения загрузки файла.

4. Приостановленное состояние (Suspended)

Приложение «спит». Система приостанавливает выполнение всего кода, сохраняя приложение в памяти для быстрого перезапуска. Если ресурсы ограничены, приложение может быть завершено.

Проблема: как заставить ваш код работать в фоновом режиме?

Когда приложение переходит в приостановленное состояние, все задачи останавливаются. Но не волнуйтесь! iOS предоставляет несколько способов продолжить выполнение задач в фоновом режиме. Давайте рассмотрим эти способы.

1. Запрос дополнительного времени для выполнения задач

Когда приложение переходит в фоновый режим, вы можете запросить дополнительное время (оно составляет около 30 секунд) для завершения важных задач, таких как загрузка файлов или сохранение данных, используя метод beginBackgroundTask. Это дает приложению временное окно для завершения операций перед переходом в приостановленное состояние.

Вот пример реализации на языке Swift:

var backgroundTask: UIBackgroundTaskIdentifier = .invalid

func startBackgroundTask() {
    backgroundTask = UIApplication.shared.beginBackgroundTask {
        // Cleanup when the background time expires
        self.endBackgroundTask()
    }

    DispatchQueue.global().async {
        self.performUploadTask()
    }
}

func performUploadTask() {
    // Simulate a file upload process
    for i in 0...100 {
        print("Uploading file: \(i)% completed")
        sleep(1)
    }
    endBackgroundTask()
}

func endBackgroundTask() {
    UIApplication.shared.endBackgroundTask(backgroundTask)
    backgroundTask = .invalid
}

При использовании beginBackgroundTask важно понимать, что система предоставляет вашему приложению ограниченное количество времени на выполнение задач (обычно около 30 секунд). Попытки обойти это ограничение времени с помощью рекурсивных функций, бесконечных циклов или других подобных хаков не сработают. Система обнаруживает и предотвращает злоупотребления с временем фонового выполнения.

2. Фоновые режимы

Некоторые типы приложений могут регистрироваться для выполнения задач в фоновом режиме. Эти режимы доступны для определенных случаев использования, например:

Чтобы включить эти режимы, настройте возможности приложения в настройках проекта

3. Бесшумные Push-уведомления

Push-уведомления могут пробудить приложение от приостановки для выполнения определенных задач. Беззвучные уведомления, которые не показываются пользователю, могут нести информацию, которую приложение обрабатывает в фоновом режиме. Пример такого пуша:

{
    "aps": {
        "content-available": 1
    },
    "data": {
        "key": "value"
    }
}

Ключ "content-available": 1 в полезной нагрузке инструктирует систему доставить уведомление без звука.

4. Использование URLSession с конфигурацией .background

Для передачи файлов, например, загрузки или скачивания файлов в фоновом режиме, Apple предоставляет API URLSession с конфигурацией .background. Такой подход идеально подходит для обработки больших загрузок файлов, которые могут продолжаться, даже если приложение приостановлено или завершено.

Интеграция фоновой загрузки в ваше приложение с помощью uploadTask

В своем домашнем проекте я использовал uploadTask для загрузки изображений на https://api.imgbb.com/. Попутно я обнаружил несколько советов и лучших практик для эффективной интеграции фоновой загрузки с помощью URLSession. Ниже я расскажу вам о советах, шагах по реализации и дополнительных примечаниях, которые помогут вам успешно добавить эту функциональность в свое приложение.

Советы, почерпнутые из других статей

1. Минимизируйте количество URLSession

Не создавайте несколько экземпляров URLSession без необходимости. Придерживайтесь одной URLSession для всех ваших запросов (или как можно меньшего количества). Почему? Система использует ограничитель скорости, чтобы управлять частотой отправки запросов в фоновом режиме. Выполняя задачи в рамках одной сессии, вы даете сигнал системе ослабить ограничитель, что позволяет быстрее выполнять последующие задачи.

Ключевой момент: Одна сессия = меньше дросселирования. Больше сеансов = больше задержек.

2. Используйте уникальные идентификаторы для URL-сессий

При создании URLSession в конфигурации .background всегда используйте уникальный идентификатор. Хорошим правилом является использование идентификатора пакета вашего приложения в качестве части имени сессии. Это предотвращает конфликты между сессиями, особенно когда несколько приложений одновременно используют фоновые задачи.

3. Задачи на основе файлов

При фоновой загрузке задачи продолжаются только в том случае, если загружаемые данные ссылаются на файл. Этот файл должен содержать тело запроса, поэтому убедитесь, что он правильно подготовлен.

4. Тщательно обрабатывайте свойство isDiscretionary

Свойство isDiscretionary в URLSessionConfiguration позволяет системе оптимизировать выполнение задач в зависимости от таких условий, как тип сети, уровень заряда батареи или наличие зарядки на устройстве. Это может быть полезно для экономии ресурсов, но это также означает, что ваши загрузки могут быть задержаны на несколько часов, если условия не идеальны (например, пользователь пользуется сотовой связью).

Совет для тестирования: Установите значение isDiscretionary на false во время разработки, чтобы избежать длительных задержек и обеспечить немедленное выполнение задач.

let config = URLSessionConfiguration.background(withIdentifier: "com.myapp.backgroundUpload")
config.isDiscretionary = false // Force immediate execution for testing

5. Тестируйте на реальном устройстве

Симуляторы ненадежны для тестирования фоновой загрузки. Всегда тестируйте на реальном устройстве, чтобы убедиться, что функция ведет себя так, как ожидается.

6. Позвольте URLSession управлять порядком выполнения задач

Нет необходимости вручную управлять порядком выполнения фоновых задач. URLSession автоматически управляет планированием задач и распределяет их по доступным очередям. Если вы загружаете несколько файлов (например, партию фотографий), просто создайте отдельный запрос для каждого файла и позвольте URLSession делать свое дело.

Пример:

for fileUrl in filesURL {
    let uploadTask = session.uploadTask(with: request, fromFile: fileURL)
    uploadTask.resume()
}

7. Остерегайтесь ограничений на размер файлов

Фоновая загрузка не рассчитана на большие файлы. Если файл слишком велик, система может отменить задание из-за таких ограничений, как низкий заряд батареи, нехватка памяти или перегрузка системы. При загрузке больших файлов подумайте о том, чтобы разбить файл на более мелкие фрагменты и загрузить их по отдельности.

Лучшее решение в этой ситуации — использовать возобновляемые сессии.

Профессиональный совет: Всегда тестируйте свои загрузки с файлами разных размеров, чтобы убедиться, что ваша реализация справится с ограничениями системы.

Пошаговая реализация

1. Включите фоновую загрузку и обработку

Включите фоновые режимы в настройках вашего приложения в Xcode:

  1. Перейдите к цели вашего прияложения
  2. Откройте вкладку Capabilities (Возможности).
  3. Включите фоновое извлечение и фоновую работу

2. Настройте URLSession

Создайте URLSession с конфигурацией .background.

private var urlSession: URLSession!

override init() {
    super.init()
    let appBundleName = Bundle.main.bundleURL.lastPathComponent.lowercased().replacingOccurrences(of: " ", with: ".")
    let configuration = URLSessionConfiguration.background(withIdentifier: appBundleName)
    configuration.sessionSendsLaunchEvents = true // Ensures the app is launched or resumed in the background when a download or upload task completes.
    configuration.isDiscretionary = false // Disables discretionary behavior, meaning the system will not delay tasks based on power or network conditions.
    urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}

3. Подготовьте тело запроса

Сохраните тело запроса во временном файле для загрузки.

Создайте многокомпонентное тело запроса:

private func createMultipartBody(imageData: Data, boundary: String) throws -> Data {
    var body = Data()
    let boundaryPrefix = "--\(boundary)\r\n"
    let base64String = imageData.base64EncodedString()

    body.append(boundaryPrefix.data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"image\"\r\n\r\n".data(using: .utf8)!)
    body.append(base64String.data(using: .utf8)!)
    body.append("\r\n".data(using: .utf8)!)
    body.append("--\(boundary)--\r\n".data(using: .utf8)!)
    return body
}

Сохранить тело запроса:

private func saveRequestBodyToTemporaryFile(_ body: Data) throws -> URL {
    let tempDirectory = FileManager.default.temporaryDirectory
    let tempFileURL = tempDirectory.appendingPathComponent(UUID().uuidString)
    try body.write(to: tempFileURL)
    return tempFileURL
}

4. Создайте таск загрузки

Инициируйте uploadTask и возобновите ее выполнение.

let uploadTask = urlSession.uploadTask(with: request, fromFile: file)
uploadTask.resume()

5. Используйте URLSessionDelegate для получения ответов

Обрабатывайте завершение задачи и полученные данные с помощью методов делегата.

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let error = error {
        print("Task completed with error: \(error.localizedDescription)")
    } else {
        print("Task completed successfully")
    }
    
    // Call completionHandler if using background sessions
    if session.tasks.isEmpty {
        self.completionHandler?()
        self.completionHandler = nil
    }
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    print("Received data: \(String(data: data, encoding: .utf8) ?? "")")
}

6. Обрабатывайте фоновые события

Убедитесь, что приложение реагирует на фоновые события и вызывает обработчик завершения.

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    self.completionHandler = completionHandler
}

Пример приложения

Это приложение предназначено для демонстрации интеграции фоновых загрузок в imgBB. Оно показывает, как:

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

Дизайн был вдохновлен WeTransfer, чтобы придать ему простой и знакомый вид.

Полный исходный код этого приложения можно найти на GitHub: https://github.com/diananareiko/backgroundUpload/tree/main

Полезные статьи

Exit mobile version