Выполнение POST-запросов и загрузки файлов с помощью URLSession

Туториалы

Выполнение POST-запросов и загрузки файлов с помощью URLSession

За прошедшие годы встроенный в Foundation URLSession API превратился в универсальный и очень мощный сетевой инструмент, настолько, что часто сторонние библиотеки теперь больше не требуются для выполнения стандартных сетевых вызовов HTTP простым и понятным способом.

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

Данные и задачи загрузки

Возможно, самый простой способ использовать URLSession для выполнения POST- запроса - это использовать основанные на URLRequest перегрузке различных API-интерфейсов dataTask (которые поддерживают делегаты и колбеки, работающие на клоужерах, а также Combine). Помимо прочего, URLRequest позволяет нам настроить, какой httpMethod должен использовать данный сетевой вызов, а также другие полезные параметры, например, какие данные httpBody отправлять и какой cachePolicy использовать. Например:

struct Networking {
    var urlSession = URLSession.shared

    func sendPostRequest(
        to url: URL,
        body: Data,
        then handler: @escaping (Result<Data, Error>) -> Void
    ) {
        // To ensure that our request is always sent, we tell
        // the system to ignore all local cache data:
        var request = URLRequest(
            url: url,
            cachePolicy: .reloadIgnoringLocalCacheData
        )
        
        request.httpMethod = "POST"
request.httpBody = body

        let task = urlSession.dataTask(
            with: request,
            completionHandler: { data, response, error in
                // Validate response and call handler
                ...
            }
        )

        task.resume()
    }
}

В зависимости от сервера, на который отправляется наш POST-запрос, мы также можем при желании дополнительно настроить наш экземпляр URLRequest, предоставив ему, например, заголовок Content-Type.

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

struct Networking {
    var urlSession = URLSession.shared

    func sendPostRequest(
        to url: URL,
        body: Data,
        then handler: @escaping (Result<Data, Error>) -> Void
    ) {
        var request = URLRequest(
            url: url,
            cachePolicy: .reloadIgnoringLocalCacheData
        )
        
        request.httpMethod = "POST"

        let task = urlSession.uploadTask(
    with: request,
    from: body,
    completionHandler: { data, response, error in
        // Validate response and call handler
        ...
    }
)

        task.resume()
    }
}

 Наблюдение за обновлениями прогресса

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

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

Чтобы наглядно это продемонстрировать, давайте создадим класс FileUploader (он должен быть подклассом NSObject Objective-C). Затем мы будем использовать собственный экземпляр URLSession, а не shared, и это позволит нам стать делегатом этой сессии. Затем мы определим API, который позволит нам загрузить файл с заданного локального URL-адреса, и далее мы позволим вызовам этого API быть переданными в виде двух клоужеров - один для обработки прогресса, другой - стандартный завершающий обработчик. Наконец, мы сохраним все обработчики прогресса загрузки в словаре на основе идентификатора загрузки, чтобы позже мы могли вызывать эти клоужеры в нашей реализации протокола делегата:

class FileUploader: NSObject {
    // We'll define a few type aliases to make our code easier to read:
    typealias Percentage = Double
    typealias ProgressHandler = (Percentage) -> Void
    typealias CompletionHandler = (Result<Void, Error>) -> Void

    // Creating our custom URLSession instance. We'll do it lazily
    // to enable 'self' to be passed as the session's delegate:
    private lazy var urlSession = URLSession(
        configuration: .default,
        delegate: self,
        delegateQueue: .main
    )

    private var progressHandlersByTaskID = [Int : ProgressHandler]()

    func uploadFile(
        at fileURL: URL,
        to targetURL: URL,
        progressHandler: @escaping ProgressHandler,
completionHandler: @escaping CompletionHandler
    ) {
        var request = URLRequest(
            url: targetURL,
            cachePolicy: .reloadIgnoringLocalCacheData
        )
        
        request.httpMethod = "POST"

        let task = urlSession.uploadTask(
            with: request,
            fromFile: fileURL,
            completionHandler: { data, response, error in
                // Validate response and call handler
                ...
            }
        )

        progressHandlersByTaskID[task.taskIdentifier] = progressHandler
        task.resume()
    }
}

Затем давайте реализуем протокол URLSessionTaskDelegate, который представляет собой специализированную версию базового протокола URLSessionDelegate, который добавляет несколько дополнительных методов, позволяющих нам отслеживать события, связанные с конкретной задачей. В этом случае мы хотим получать уведомления только о том, когда был обновлен ход выполнения данной задачи URLSessionTask, и это можно сделать, реализовав следующий метод:

extension FileUploader: URLSessionTaskDelegate {
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64,
        totalBytesExpectedToSend: Int64
    ) {
        let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
        let handler = progressHandlersByTaskID[task.taskIdentifier]
        handler?(progress)
    }
}

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

Показатель загрузки процесса во времени

Наконец, давайте также посмотрим, как можно преобразовать указанный выше FileUploader для использования Combine вместо нескольких замыканий. В конце концов, дизайн Combine, ориентированный на «значения с течением времени», идеально подходит для моделирования обновлений прогресса, поскольку мы хотим отправить какие-то процентные значения с течением времени, а затем закончить событие завершением, что и является тем, что делает publisher Combine.

Хотя мы могли бы реализовать эту функцию с помощью кастомного publisher, давайте в данном случае воспользуемся CurrentValueSubject, который предоставляет встроенный способ отправки значений, которые затем кэшируются и отправляются каждому новому подписчику. Таким образом, мы можем связать каждую задачу загрузки с заданным субъектом (точно так же, как мы ранее хранили каждый клоужер progressHandler), а затем вернуть этот субъект в качестве publisher с помощью API eraseToAnyPublisher - например:

class FileUploader: NSObject {
    typealias Percentage = Double
    typealias Publisher = AnyPublisher<Percentage, Error>
    
    private typealias Subject = CurrentValueSubject<Percentage, Error>

    private lazy var urlSession = URLSession(
        configuration: .default,
        delegate: self,
        delegateQueue: .main
    )

    private var subjectsByTaskID = [Int : Subject]()

    func uploadFile(at fileURL: URL,
                    to targetURL: URL) -> Publisher {
        var request = URLRequest(
            url: targetURL,
            cachePolicy: .reloadIgnoringLocalCacheData
        )
        
        request.httpMethod = "POST"

        let subject = Subject(0)
        var removeSubject: (() -> Void)?

        let task = urlSession.uploadTask(
            with: request,
            fromFile: fileURL,
            completionHandler: { data, response, error in
                // Validate response and send completion
                ...
                subject.send(completion: .finished)
                removeSubject?()
            }
        )

        subjectsByTaskID[task.taskIdentifier] = subject
        removeSubject = { [weak self] in
            self?.subjectsByTaskID.removeValue(forKey: task.taskIdentifier)
        }
        
        task.resume()
        
        return subject.eraseToAnyPublisher()
    }
}

Теперь все, что осталось - это обновить нашу реализацию URLSessionTaskDelegate для отправки каждого значения прогресса субъекту, связанному с рассматриваемой задачей, вместо вызова замыкания:

extension FileUploader: URLSessionTaskDelegate {
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64,
        totalBytesExpectedToSend: Int64
    ) {
        let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
        let subject = subjectsByTaskID[task.taskIdentifier]
        subject?.send(progress)
    }
}

Теперь мы можем легко выполнять как более простые POST-запросы, так и загрузку файлов с отображением прогресса загрузки, используя либо Combine, либо API на основе замыканий. Классяче!

Оригинал статьи

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: