The URLSession API of the Foundation framework is a modern, first-party API to perform HTTP requests in Swift. There is no need for third-party libraries, such as Alamofire, if you need to make a few HTTP requests to an API. Foundation's URLSession API offers everything you need.
In a previous tutorial, I wrote about making a GET request using the URLSession API. In this tutorial, you learn how to make a POST request, another common operation in client apps.
In this tutorial, we take a look at three APIs, (1) a completion handler, (2) Swift concurrency, and (3) a reactive approach using the Combine framework.
Creating a Playground
Fire up Xcode and create a playground by selecting New > Playground... from Xcode's File menu. Choose the Blank template from the iOS > Playground section.
Remove the contents of the playground with the exception of the import statement for the UIKit framework at the top. The URLSession
class is defined in the Foundation framework. Importing UIKit automatically imports Foundation.
import UIKit
Working with URLSession
If you want to learn more about URLSession
, then I recommend reading How to Make an HTTP Request in Swift. In that tutorial, we take a closer look at the URLSession API. In this tutorial, we focus on the steps you need to take to make a PUT or a POST request using the URLSession API.
The goal of this tutorial is to send a JSON object to a web service. If we want to send data to a web service, we need to make a PUT or a POST request. In other words, the HTTP verb is PUT or POST. We make a POST request in this tutorial, but the only difference with a PUT request is the HTTP method. Don't worry if this sounds confusing. It becomes clear in a few moments.
Option 1: Completion Handler
We first define the URL for the endpoint of the web service we send the request to. I set up a mock web service for this tutorial. The URL of the mock web service is https://cocoacasts.com/mockapi. The endpoint we use in this tutorial is the message endpoint, so the URL for the POST request is https://cocoacasts.com/mockapi/message. Note that we force unwrap the URL
object the initializer returns. That is fine for this tutorial.
import UIKit
let url = URL(string: "https://cocoacasts.com/mockapi/message")!
The next step is creating a URLRequest
object. We create a mutable request because we need to configure the request before we send it.
import UIKit
let url = URL(string: "https://cocoacasts.com/mockapi/message")!
var request = URLRequest(url: url)
The first property we set is the httpMethod
property. To send a POST request to the mock web service, we set the httpMethod
property of the URLRequest
object to POST.
import UIKit
let url = URL(string: "https://cocoacasts.com/mockapi/message")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
The message endpoint expects a JSON object with three fields, userID
, toUserID
, and message
. The value of userID
is the ID of the sender of the message, the value of toUserID
is the ID of the recipient of the message, and the value of message
is the message that is sent. The message endpoint returns a 200
response on success with no content.
We first create a struct with name Message
that defines the body or payload of the request. We conform the struct to the Encodable
protocol to convert an instance of the struct to a Data
object. Why that is necessary becomes clear in a few moments.
struct Message: Encodable {
let userID: Int
let toUserID: Int
let message: String
}
We create a Message
object and use a JSONEncoder
instance to convert it to a Data
object. We cannot send the Message
object as is. It needs to be converted to a Data
object. That is why we conformed the Message
struct to the Encodable
protocol.
let message = Message(
userID: 123,
toUserID: 456,
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
)
let data = try! JSONEncoder().encode(message)
Note that we use the try!
operator to avoid having to deal with optionals in the playground. In production, you would handle any errors that could be thrown to fail gracefully.
With the Data
object created, we can attach it to the request by setting its httpBody
property.
request.httpBody = data
We need to make it clear to the mock web service that we are sending a JSON object. That is typically done by setting the Content-Type header of the request. We invoke the setValue(_:forHTTPHeaderField:)
method on the URLRequest
object, passing in application/json
as the value and Content-Type
as the name of the header.
request.setValue(
"application/json",
forHTTPHeaderField: "Content-Type"
)
To send the request to the mock web service, we ask the shared URLSession
instance to create a data task by invoking the dataTask(with:completionHandler:)
method. It returns a URLSessionDataTask
instance and accepts two arguments, a URLRequest
object and a completion handler. The completion handler, a closure, is executed when the data task completes, successfully or unsuccessfully. The completion handler accepts three arguments, an optional Data
object, an optional URLResponse
object, and an optional Error
object. The implementation is similar to the one in How to Make an HTTP Request in Swift. The only difference is that we pass a URLRequest
object to the dataTask(with:completionHandler:)
method instead of a URL
object.
let task = URLSession.shared.dataTask(with: request) { data, response, error in
}
We are only interested in the URLResponse
object. To know if the request was successful, we inspect the status code of the response. This is a bit clunky because the response is of type URLResponse
. We cast the response to HTTPURLResponse
and read the value of its statusCode
property. Note that we use the as!
operator to cast the response to HTTPURLResponse
. The cast should never fail. The request is successful if the status code is equal to 200
.
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let statusCode = (response as! HTTPURLResponse).statusCode
if statusCode == 200 {
print("SUCCESS")
} else {
print("FAILURE")
}
}
Remember from How to Make an HTTP Request in Swift that we need to call resume()
on the URLSessionDataTask
instance to execute it.
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let statusCode = (response as! HTTPURLResponse).statusCode
if statusCode == 200 {
print("SUCCESS")
} else {
print("FAILURE")
}
}
task.resume()
Run the contents of the playground and inspect the output in the console. You should see SUCCESS printed to the console.
SUCCESS
Option 2: Swift Concurrency
The completion handler we passed to the dataTask(with:completionHandler:)
method is invoked when the request completes, successfully or unsuccessfully. That works fine, but there is a more elegant and modern API that leverages Swift concurrency.
Sending the POST request is an asynchronous operation and we do that in a Task
. In the body of the Task
, we invoke the data(for:)
method on the shared URLSession
instance. The data(for:)
method accepts a URLRequest
object. It is a throwing method, so we wrap it in a do-catch
statement. We prefix the method call with the await
keyword because the method is asynchronous.
Task {
do {
let (_, response) = try await URLSession.shared.data(for: request)
} catch {
print("Failed to Send POST Request \(error)")
}
}
The method returns a tuple with two values, a Data
object and a URLResponse
object. We are not interested in the Data
object. As before, we use the URLResponse
object to figure out whether the POST request was successful.
Task {
do {
let (_, response) = try await URLSession.shared.data(for: request)
let statusCode = (response as! HTTPURLResponse).statusCode
if statusCode == 200 {
print("SUCCESS")
} else {
print("FAILURE")
}
} catch {
print("Failed to Send POST Request \(error)")
}
}
The beauty of using Swift concurrency is that we can read this snippet of code from top to bottom. That isn't true if we use a completion handler. Another major benefit is how errors are handled. The method is throwing, which forces us to be conscious of any errors that may be thrown if something goes wrong. What I like most, however, is the fact that the Data
and URLResponse
objects of the tuple are not optionals. If the POST request succeeds, then we are guaranteed to have access to a Data
object and a URLResponse
object. Remember that the Data
, URLResponse
, and Error
objects of the completion handler are optionals. That isn't ideal for several reasons.
Option 3: Reactive Programming with Combine
There is one last option I would like to show you. I am a big fan of reactive programming and the Combine framework is Apple's implementation of a reactive framework for Apple development. We can ask the shared URLSession
instance for a publisher that emits the response of the POST request or, if the request fails, terminates with an error.
URLSession.shared.dataTaskPublisher(for: request)
We invoke the sink(receiveCompletion:receiveValue:)
method to subscribe to the publisher. The completion handler, the first argument of the sink(receiveCompletion:receiveValue:)
method, is invoked when the request completes, successfully or unsuccessfully. We can get access to the Data
and URLResponse
objects through the value handler, the second argument of the sink(receiveCompletion:receiveValue:)
method.
let cancellable = URLSession.shared.dataTaskPublisher(for: request)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
()
case .failure(let error):
print("Failed to Send POST Request \(error)")
}
}, receiveValue: { _, response in
let statusCode = (response as! HTTPURLResponse).statusCode
if statusCode == 200 {
print("SUCCESS")
} else {
print("FAILURE")
}
})
The sink(receiveCompletion:receiveValue:)
method returns an AnyCancellable
. We need to hold on to the AnyCancellable
until the request completes. For that reason, we store a reference to the AnyCancellable
in a constant with name cancellable
.
What's Next?
In this tutorial, we explored three URLSession APIs to send a POST request to a web service. While each API has its pros and cons, the async data(for:)
method and the reactive dataTaskPublisher(for:)
method are the preferred and more modern solutions. My recommendation is to use the async data(for:)
method unless you need a reactive solution. That said, nothing stops you from using the API that uses a completion handler.