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.

How to Make an HTTP Request in Swift

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.

Fetching a Remote Image in Swift

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.