Articles, podcasts and news about Swift development, by John Sundell.

Performing POST and file upload requests using URLSession

Published on 06 Jan 2021
Basics article available: Networking

Over the years, Foundation’s built-in URLSession API has grown to become a versatile and very powerful networking tool, to the point where third party libraries are often no longer required to perform standard HTTP network calls in a simple and straightforward way.

While many of the convenience APIs that URLSession ships with are focused on GET requests used to fetch data, in this article, let’s take a look at how other HTTP methods can be used as well — specifically how different kinds of POST requests can be performed without any external dependencies.

Data and upload tasks

Perhaps the simplest way to use URLSession to perform a POST request is to use the URLRequest-based overloads of its various dataTask APIs (which support both delegate and closure-based callbacks, as well as Combine). Among many other things, URLRequest enables us to customize what httpMethod that a given network call should use, as well as other useful parameters, such as what httpBody data to send, and what cachePolicy to use. Here’s an example:

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()
    }
}

Depending on the server that our POST request is being sent to, we might also want to configure our URLRequest instance further, for example by giving it a Content-Type header.

Alternatively, we could instead choose to use the uploadTask API to create our request task, which both lets us upload data while the app is in the background, and provides built-in support for attaching body data directly to the task itself:

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()
    }
}

Observing progress updates

While either of the above two approaches will work perfectly fine when sending smaller amounts of data as part of a POST request, sometimes we might want to upload a file that could potentially be quite large (even something simple, like an image, could easily be several Megabytes in size). When doing that, we likely want to give the user some form of real-time progress updates, since otherwise our app’s UI might appear slow or even unresponsive.

Unfortunately, none of the closure-based or Combine-powered URLSession APIs offer direct support for observing a request’s ongoing progress, but thankfully, that’s something that we can implement quite easily using the good old fashioned delegate pattern.

To demonstrate, let’s create a FileUploader class (which needs to be a subclass of Objective-C’s NSObject). We’ll then use a custom URLSession instance, rather than the shared one, since that’ll let us become the delegate of that session. We’ll then define an API that’ll let us upload a file from a given local URL, and we’ll let the callers of that API pass in two closures — one for handling progress events, as well as a standard completion handler. Finally, we’ll store all progress event handlers in a dictionary based on each upload task’s ID, so that we’ll later be able to call those closures within our delegate protocol implementation:

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()
    }
}

Next, let’s implement the URLSessionTaskDelegate protocol, which is a specialized version of the base URLSessionDelegate protocol that adds a few extra methods to enable us to observe task-specific events. In this case, we only want to be notified when the progress of a given URLSessionTask was updated, which can be done by implementing the following method:

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)
    }
}

With the above in place, we’ll now be able use the percentage values passed into each progressHandler closure to drive any UI component that we want to use to visualize the progress of an upload — such as a ProgressView, UIProgressView, or NSProgressIndicator.

A stream of progress over time

Finally, let’s also take a look at how we could convert the above FileUploader to use Combine instead of multiple closures. After all, Combine’s "values over time”-focused design is quite a perfect fit for modeling progress updates, since we want to send a number of percentage values over time, and to then end with a completion event, which is exactly what a Combine publisher does.

While we could choose to implement this functionality using a custom publisher, let’s use CurrentValueSubject in this case, which provides a built-in way to send values that then get cached and sent to every new subscriber. That way, we can associate each upload task with a given subject (just like how we previously stored each progressHandler closure) and then return that subject as a publisher using the eraseToAnyPublisher API — like this:

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()
    }
}

Now all that remains is to update our URLSessionTaskDelegate implementation to send each progress value to the subject associated with the task in question, rather than calling a closure:

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)
    }
}

Just like that, we can now easily perform both simpler POST requests and file uploads, with progress events, using either Combine or a closure-based API. Really nice!

Conclusion

While the above series of implementations are not a complete networking library by any stretch of the imagination, they’ve hopefully demonstrated how the built-in functionality that URLSession provides is often all that we need to perform many different kinds of requests, including those that involve posting data or uploading files.

I hope that you found this article useful. If you have any questions or comments, then feel free to reach out via either Twitter or email. Thanks for reading!