Главное - в мелочах

URLSession позволяет загружать и выгружать файлы в фоновом режиме. Хотя основы кажутся простыми, довольно сложно сделать это правильно. Из доступных ресурсов и документации можно выделить несколько мелочей, которые действительно важны для работы фоновой загрузки.

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

Общие ловушки

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

1. Каждой конфигурации URLSessionConfiguration требуется уникальный идентификатор.

В основном это проблема, если вы запускаете фоновые загрузки или выгрузки из расширения приложения, когда ваше основное приложение перезапускается с идентификатором, переданным следующим методом:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void)

Вы можете просто проигнорировать переданный идентификатор и перестроить URLSession, как и для вашего хост-приложения. Однако с расширениями приложений вы имеете дело с несколькими экземплярами URLSession. Каждому из них нужен свой собственный идентификатор, и этот идентификатор также необходимо использовать при восстановлении сеанса.

Я пришел к такому выводу после прочтения этой документации Apple, в которой говорится:

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

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

/// Contains any `URLSession` instances associated with app extensions.
private lazy var appExtensionSessions: [URLSession] = []

/// Creates an identical `URLSession` for the given identifier or returns an existing `URLSession` if it was already registered.
///
/// - Parameter identifier: The `URLSessionConfiguration` identifier to use for recreating the `URLSession`.
/// - Returns: A newly created or existing `URLSession` instance matching the given identifier.
private func session(for identifier: String) -> URLSession {
    if let existingSession = appExtensionSessions.first(where: { $0.configuration.identifier == identifier }) {
        return existingSession
    } else {
        let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
        configuration.sharedContainerIdentifier = appGroup
        let appExtensionSession = URLSession(configuration: configuration, delegate: self, delegateQueue: sessionOperationQueue)
        appExtensionSessions.append(appExtensionSession)
        return appExtensionSession
    }
}

2. Идентификаторы на основе пакетов

Чтобы не рисковать, вы можете использовать следующий код, чтобы убедиться, что ваш идентификатор URLSessionConfiguration всегда уникален:

let appBundleName = Bundle.main.bundleURL.lastPathComponent.lowercased().replacingOccurrences(of: " ", with: ".")
let sessionIdentifier: String = "com.wetransfer.networking.\(appBundleName)"
let configuration = URLSessionConfiguration.background(withIdentifier: sessionIdentifier)

3. Не забывайте идентификатор общего контейнера.

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

Вы можете установить идентификатор следующим образом:

let configuration = URLSessionConfiguration.background(withIdentifier: "swiftlee.background.url.session")
configuration.sharedContainerIdentifier = "group.swiftlee.apps"

4. Поддерживается только загрузка задач из файла.

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

5. Параметр isDiscretionary может навредить

Я помню, как сижу за столом и смотрю на экран. Загрузки не работали, пока я не подключил зарядное устройство. Прочитав документацию почти по каждому методу и параметру, я обнаружил свойство isDiscretionary:

«Для объектов конфигурации, созданных с использованием метода background(withIdentifier:), используйте это свойство, чтобы дать системе контроль над тем, когда должна происходить передача».

Это уже заставило меня задуматься. Чтение дальше:

«При передаче больших объемов данных рекомендуется установить для этого свойства значение true. Это позволяет системе планировать эти передачи в более оптимальное для устройства время. Например, система может отложить передачу больших файлов до тех пор, пока устройство не будет подключено и подключено к сети через Wi-Fi ».

И это имело смысл!

Для ясности, в некоторых случаях полезно установить для этого свойства значение true. Однако для нашего случая использования приложения Collect нам потребовалась мгновенная загрузка и выгрузка. Поэтому нам пришлось вернуть для свойства значение по умолчанию false.

Заключение

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

Спасибо!