Performing POST and file upload requests using URLSession
Basics article available: NetworkingOver 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!