There are plenty of third-party libraries you can use to perform HTTP requests in Swift, but I always default to Foundation's URLSession API. It is a first-party API and easy to use. The fewer dependencies a project has, the better. Right?
In this tutorial, you learn how easy it is to perform an HTTP request in Swift using the URLSession API. I show you how to fetch a remote image and JSON data. 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
URLSession
replaces NSURLConnection
. The URLSession
API is easier to use and modern. It is available on iOS, tvOS, macOS, and watchOS. URLSession
is one class of the URLSession
API. There are a handful of types you need to become familiar with to perform an HTTP request in Swift.
A URLSession
instance is the manager or coordinator of the requests your application performs. A request is referred to as a task, an instance of the URLSessionTask
. You never directly use the URLSessionTask
class. Foundation defines a number of URLSessionTask
subclasses. Each subclass has a specific objective, such as downloading or uploading data.
Option 1: Completion Handler
Creating a Data Task
Let's use the URLSession API to perform an HTTP request. The objective is to fetch the data for an image. Before we start, we need to define the URL of the remote image.
import UIKit
let url = URL(string: "https://bit.ly/2LMtByx")!
The next step is creating a data task, an instance of the URLSessionDataTask
class. A data task is always tied to a URLSession
instance. To keep things simple, we ask the URLSession
class for the shared URLSession
instance, a singleton, through its shared
class property.
URLSession.shared
We then ask the shared URLSession
instance to create a data task by invoking the dataTask(with:completionHandler:)
method. This method returns a URLSessionDataTask
instance and accepts two arguments, a URL
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.
import UIKit
let url = URL(string: "https://bit.ly/2LMtByx")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
}
A data task fails or succeeds. If the data task fails, then error
has a value. If the data task succeeds, then data
and response
have a value. We are not interested in the URLResponse
object for now. We safely unwrap the Data
object and use it to create a UIImage
instance. If data
is equal to nil
, then the HTTP request failed and we print the value of error
to the console.
import UIKit
let url = URL(string: "https://bit.ly/2LMtByx")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
let image = UIImage(data: data)
} else if let error = error {
print("HTTP Request Failed \(error)")
}
}
Even though we created a data task, the HTTP request isn't executed. We need to call resume()
on the URLSessionDataTask
instance to execute it.
import UIKit
let url = URL(string: "https://bit.ly/2LMtByx")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
let image = UIImage(data: data)
} else if let error = error {
print("HTTP Request Failed \(error)")
}
}
task.resume()
If you run the contents of the playground and wait a few seconds, you should see the result of the HTTP request appear in the results panel on the right. The dimensions of the UIImage
instance are displayed in the results panel. No error is printed to the console, which means the HTTP request was successfully executed.
Creating a Request
We created a data task by passing a URL
object to the dataTask(with:completionHandler:)
method. This works fine, but it doesn't offer a lot of flexibility. The URLSession
class defines another method that accepts a URLRequest
object. As the name suggests, the URLRequest
struct encapsulates the information the URL session needs to perform the HTTP request. Let me show you how this works.
We create a URLRequest
object by passing the URL
object to the initializer and store the result in a variable with name request
. We modify the URLRequest
object in a moment hence var
instead of let
.
var request = URLRequest(url: url)
We pass the URLRequest
object to the dataTask(with:completionHandler:)
method. The name of the method is similar to the one we used earlier. The difference is that it accepts a URLRequest
object instead of a URL
object.
import UIKit
let url = URL(string: "https://bit.ly/2LMtByx")!
var request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
let image = UIImage(data: data)
} else if let error = error {
print("HTTP Request Failed \(error)")
}
}
task.resume()
Execute the contents of the playground. Notice that the result is identical. You may wonder what we gain by using URLRequest
. The URLRequest
object allows us to configure the HTTP request the URL session performs. We can set the HTTP method, through the httpMethod
property. This isn't necessary in this example since it defaults to GET
.
request.httpMethod = "GET"
We can also define the request's HTTP header fields. If you need to override the default HTTP header fields, then you set the allHTTPHeaderFields
property, a dictionary of type [String:String]
. This is also important if you need to deal with authentication. In this example, we add an HTTP header field to pass an API key to the server we are communicating with.
request.allHTTPHeaderFields = [
"X-API-Key": "123456789"
]
You can also set the request's HTTP header fields by invoking the setValue(_:forHTTPHeaderField:)
method. This is a mutating method that sets the value for a given HTTP header field.
request.setValue("application/png", forHTTPHeaderField: "Content-Type")
Requesting JSON with URLSession
Fetching JSON is another common example. This isn't any more difficult than fetching the data for a remote image. We update the URL
object and set the value of the Content-Type HTTP header field to application/json.
import UIKit
let url = URL(string: "https://bit.ly/3sspdFO")!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
We have a few options to decode the response. I don't recommend using the JSONSerialization
class unless you have a very good reason. The preferred and recommended approach is the Decodable
protocol in combination with the JSONDecoder
class.
We define a struct with name Book
that conforms to the Decodable
protocol. The Book
struct defines two properties, title
of type String
and author
of type String
.
import UIKit
struct Book: Decodable {
let title: String
let author: String
}
let url = URL(string: "https://bit.ly/3sspdFO")!
var request = URLRequest(url: url)
request.setValue(
"application/json",
forHTTPHeaderField: "Content-Type"
)
In the completion handler, we create a JSONDecoder
instance and invoke the decode(_:from:)
method, passing in the type of the value to decode from the supplied JSON object and the JSON object to decode as a Data
object. We print the result to the console. Execute the contents of the playground to see the result.
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let books = try? JSONDecoder().decode([Book].self, from: data) {
print(books)
} else {
print("Invalid Response")
}
} else if let error = error {
print("HTTP Request Failed \(error)")
}
}
task.resume()
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.
Performing an HTTP 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 (data, _) = try await URLSession.shared.data(for: request)
} catch {
print("Failed to Send POST Request \(error)")
}
}
The data(for:)
method returns a tuple with two values, a Data
object and a URLResponse
object. We are only interested in the Data
object. As before, we convert the Data
object to an array of Book
objects with the help of a JSONDecoder
instance.
Task {
do {
let (data, _) = try await URLSession.shared.data(for: request)
if let books = try? JSONDecoder().decode([Book].self, from: data) {
print(books)
} else {
print("Invalid Response")
}
} catch {
print("Failed to Send POST Request \(error)")
}
}
Swift concurrency comes with several interesting benefits. First, you can read the code we wrote from top to bottom. That makes the code easier to understand and reason about. Second, error handling is built into Swift concurrency. We don't need to unwrap an optional Error
object to figure out whether the request was successful. If the request fails, an error is thrown. Simple. Right? Third, the values of the tuple are not optionals. If the GET request succeeds, then we are guaranteed to have access to a Data
object and a URLResponse
object.
Option 3: Reactive Programming with Combine
The URLSession API neatly integrates with Combine, Apple's reactive framework. We can ask the shared URLSession
instance for a publisher that emits the response of the GET request. If the request fails, then the publisher terminates or completes with an error.
URLSession.shared.dataTaskPublisher(for: request)
We subscribe to the publisher the dataTaskPublisher(for:)
method returns by invoking the sink(receiveCompletion:receiveValue:)
method. The method accepts two arguments, a completion handler that is invoked when the publisher completes and a value handler that is invoked when the response of the request is available. We can get access to the Data
and URLResponse
objects through the value handler.
let cancellable = URLSession.shared.dataTaskPublisher(for: request)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
()
case .failure(let error):
print("Failed to Send GET Request \(error)")
}
}, receiveValue: { data, _ in
if let books = try? JSONDecoder().decode([Book].self, from: data) {
print(books)
} else {
print("Invalid Response")
}
})
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
.
Scratching the Surface
In this tutorial, we explored three APIs to perform an HTTP request in Swift using the URLSession API. I hope you agree that this isn't rocket science. The URLSession API is easy to use and, if you learn more about the API, flexible and powerful. We only scratched the surface in this tutorial. Take a look at How to Make a POST Request in Swift if you want to learn how to perform a PUT or POST request using the URLSession API.