Back to blog

Networking in Swift with URLSession


Aasif Khan
By Aasif Khan | Last Updated on November 24th, 2023 6:43 am | 5-min read

In this app development tutorial, we’ll discuss how you can use the URLSession suite of components, classes and functions to make HTTP GET and POST network requests. You’ll learn how to validate response data, and how to add additional parameters like headers to requests.

Almost every app will communicate with the internet at some point. How does that work? What Swift code can you use to make HTTP(S) networking requests?

Fetching and downloading data from and to webservices is a skill any pragmatic iOS developer must master, and URLSession offers a first-party best-in-class API to make networking requests.

Describe your app idea
and AI will build your App

How Making HTTP Requests Works On iOS

Imagine you’re making a Twitter app. At some point, the app needs to request data from Twitter’s API. When a user views their timeline, you could use the Twitter API to get information about their tweets.

The Twitter API is a webservice that responds to HTTP(S) requests. On iOS, we can use the URL Loading System to configure and make HTTP requests. This is called networking, and it’s a staple in any modern iOS app – almost all apps communicate with servers on the internet, at some point.

Quick Note: From a security perspective, it’s important you get into the habit of defaulting to HTTPS, i.e. networking requests encrypted with SSL/TLS, when working with URLs. You can get SSL certificates for free via Let’s Encrypt.

Since iOS 7, the de facto way of making HTTP networking requests is by using a class called URLSession. The URLSession class is actually part of a group of classes that work together to make and respond to HTTP requests.

Many developers also rely on 3rd-party libraries, such as Alamofire, but you’ll soon find out that you don’t need to depend on a library for simple HTTP networking. The URLSession class has everything we need already.

Here’s how the environment works:

  • You use URLSession to create a session. You can think of a session as an open tab or window in your webbrowser, which groups together many HTTP requests over subsequent website visits.
  • The URLSession is used to create URLSessionTask instances, which can fetch and return data to your app, and download and upload files to webservices.
  • You configure a session with a URLSessionConfiguration object. This configuration manages caching, cookies, connectivity and credentials.
  • To make a request, you create a data task, of class URLSessionDataTask, and you provide it with a URL, such as https://twitter.com/api/, and a completion handler. This is a closure that’s executed when the request’s response is returned to your app.
  • When the completion handler is executed, you can inspect the returned data and take appropriate action, such as loading the data into a Tweet UI.

You use a URLSession to make multiple subsequent requests, as URLSessionTask instances. A task is always part of a session. The URLSession also kinda functions like a factory that you use to set up and execute different URLSessionTasks, based on the parameters you put in.

With URLSession, we differentiate between three kinds of tasks:

  • Data tasks send and receive data with URLSessionDataTask, by using NSData objects. They’re the most common for webservice requests, for example when working with JSON.
  • Upload tasks send files to a webserver with URLSessionUploadTask. They’re similar to data tasks, but URLSessionUploadTask instances can also upload data in the background (or when an app is suspended).
  • Download tasks download files from a webserver with URLSessionDownloadTask by directly writing to a temporary file. You can track the progress of file downloads, and pause and resume them.

Let’s get started with making a few networking requests with URLSession!

The official Apple documentation for URLSession is extensive, but it’s not as organized as you’d want. A good starting point is URL Loading System, and subsequently reading linked articles, such as Fetching Website Data into Memory. And if you want a primer on how to make the most of Apple’s Developer Documentation, make sure to read How To Use Apple’s Developer Documentation For Fun And Profit.

Fetching Data With URLSession

Let’s fetch some data with URLSession! Here’s what we’re going to do:

  1. Set up the HTTP request with URLSession
  2. Make the request with URLSessionDataTask
  3. Quickly print the returned response data
  4. Properly validate the response data
  5. Convert the response data to JSON

Set up the HTTP request with URLSession

First, we’ll need to set up the request we want to make. As discussed before, we’ll need a URL and a session. Like this:

let session = URLSession.shared
let url = URL(string: “…”)!
The URL we’ll request is users.json (Right-click, then Copy Link).

In the above code we’re initializing a url constant of type URL. The initializer we’re using is failable, but since we’re certain the URL is correct, we use force-unwrapping to deal with the optional.

The URLSession.shared singleton is a reference to a shared URLSession instance that has no configuration. It’s more limited than a session that’s initialized with a URLSessionConfiguration object, but that’s OK for now.

Make the request with URLSessionDataTask

Next, we’re going to create a data task with the dataTask(with:completionHandler:) function of URLSession. Like this:

let task = session.dataTask(with: url, completionHandler: { data, response, error in

// Do something…
})
A few things are happening here. First, note that we’re assigning the return value of dataTask(with:completionHandler:) to the task constant. This is that data task, as discussed earlier, of type URLSessionDataTask.

The dataTask(with:completionHandler:) has two parameters: the request URL, and a completion handler. We’ve created that request URL earlier, so that’s easy.

The completion handler is a bit more complicated. It’s a closure that’s executed when the request completes, so when a response has returned from the webserver. This can be any kind of response, including errors, timeouts, 404s, and actual JSON data.

The closure has three parameters: the response Data object, a URLResponse object, and an Error object. All of these closure parameters are optionals, so they can be nil.

Each of these parameters has a distinct purpose:

  • We can use the data object, of type Data, to check out what data we got back from the webserver, such as the JSON we’ve requested
  • The response object, of type URLResponse, can tell us more about the request’s response, such as its length, encoding, HTTP status code, return header fields, etcetera
  • The error object contains an Error object if an error occurred while making the request. When no error occurred, it’s simply nil.

The nature of HTTP requests is flaky, to say the least. You’ll need to validate anything you get back: errors, expected HTTP status codes, malformed JSON, and so on. You could get a 200 OK response back, with HTML, even though you expected JSON. You’ll see how we deal with this, later on.

At this point, the network request hasn’t been executed yet! It has only been set up. Here’s how you start the request:

task.resume()

By calling the resume() function on the task object, the request is executed and the completion handler is invoked at some point. It’s easy to forget calling resume(), so make sure you don’t!

You can use delegation with URLSessionDelegate instead of completion handlers. I personally find using closures more convenient, especially because you can use promises and PromiseKit to deal with them more easily.

Print the returned response data

Just for fun, let’s check out what we’re actually getting back in the completion handler. Here’s the relevant code:

let task = session.dataTask(with: url) { data, response, error in
print(data)
print(response)
print(error)
}
Quick Note: The above snippet uses trailing closure syntax. When a function’s last parameter accepts a closure, you can write that closure outside the functions parentheses (). Makes it much easier to read!

When the above code is executed, this is printed out:

  • The data value prints something like Optional(321 bytes). Hmm, why is that? It’s because data is a Data object, so it has no visual representation yet. We can convert or interpret it as JSON though, but that requires some more code.
  • The response is of type NSHTTPURLResponse, a subclass of URLResponse, and it contains a ton of data about the response itself. The HTTP status code is 200, and from the HTTP headers we can see that this request passed through Cloudflare.
  • And the error? It’s nil. Fortunately, no errors were passed to the completion handler. That doesn’t mean the request is OK, though!

Properly validate the response data

OK, let’s do some validation in the completion handler. When you’re making HTTP networking requests, you’ll need to validate the following at least:

  • Did any errors occur? You can check this with the passed error object.
  • Is the HTTP response code expected?
  • Did you get data in the right format?

First, let’s check if error is nil or not. Here’s how:

if error != nil {
// OH NO! An error occurred…
self.handleClientError(error)
return
}

What should you do inside the error != nil conditional? Two recommendations:

  • Call a function that can deal with the response, and take appropriate action, like the example above
  • Throw an error with throw and use promises to deal with any thrown or passed errors in the chain’s .catch clause

Then, let’s check if the HTTP status code is OK. Here’s how:

guard let httpResponse = response as? HTTPURLResponse,
(200…299).contains(httpResponse.statusCode) else {
self.handleServerError(response)
return
}

This is what happens:

  • The guard let syntax checks if the two conditions evaluate to false, and when that happens the else clause is executed. It literally “guards” that these two conditions are true. It’s easiest to read this as: “Guard that response is a HTTPURLResponse and statusCode is between 200 and 299, otherwise call handleServerError().”
  • The first condition is an optional downcast from response of type URLResponse to HTTPURLResponse. This downcast ensures we can use the statusCode property on the response, which is only part of HTTPURLResponse.
  • The range (200…299) is a sequence of HTTP status codes that are regarded as OK. You can check all HTTP status codes here. So, when statusCode is contained in 200…299, the response is OK.
  • When it’s not OK, for example if we get a 500 Internal Server Error, the function handleServerError() is called and we return the closure.

The next validation we’re going to do, checks the so-called MIME type of the response. This is a value that most webservers return, that explains what the format of the response data is. We’re expecting JSON, so that’s what we’ll check:

guard let mime = response.mimeType, mime == “application/json” else {
print(“Wrong MIME type!”)
return
}
The above code uses the same guard let syntax to make sure that response.mimeType equals application/json. When it doesn’t, we’ll need to respond appropriately and attempt to recover from the error.

You see that there’s a great number of errors that can occur, and you’ll need to validate most if not all of them. And we haven’t even dealt with application errors, such as “Incorrect password!” or “Unknown User ID!” It’s a smart idea to consider what kind of errors you’ll encounter, and to come up with a strategy or model to deal with them consistently and reliably.

Convert the response data to JSON

Now that we’re sure that the response is OK, we can parse it to a JSON object. Here’s how:

if let json = try? JSONSerialization.jsonObject(with: data!, options: []) {
print(json)
}
And here’s what happens in the above code:

  • We’re using the jsonObject(with:options:) function of the JSONSerialization class to serialize the data to JSON. Essentially, the data is read character by character and turned into a JSON object we can more easily read and manipulate. It’s similar to how you read a book word by word, and then turn that into a story in your head.
  • The optional binding with try? is a trick you can temporarily use to silence any errors from jsonObject(…). In short, errors can occur during serialization, and when they do, the return value of jsonObject(…) is nil, and the conditional doesn’t continue executing.

It’s worth noting here that the following is the proper way to deal with errors:

do {
let json = try JSONSerialization.jsonObject(with: data!, options: [])
print(json)
} catch {
print(“JSON error: \(error.localizedDescription)”)
}
You don’t have to use JSONSerialization; a superb alternative is using Codable to decode JSON data. Neat!

In the above code, errors thrown from the line marked with try are caught in the catch block. We also could have rethrown the error, and dealt with it in another part of the code.

When the JSON data is OK, it’s assigned to the json constant and printed out. And we can finally see that this is the response data from that URL we started with:

(
{
age = 5000;
“first_name” = Ford;
“last_name” = Prefect;
},
{
age = 999;
“first_name” = Zaphod;
“last_name” = Beeblebrox;
},
{
age = 42;
“first_name” = Arthur;
“last_name” = Dent;
},
{
age = 1234;
“first_name” = Trillian;
“last_name” = Astra;
}
)
Awesome! And here’s the complete code we’ve written so far:

let session = URLSession.shared
let url = URL(string: “…”)!

let task = session.dataTask(with: url) { data, response, error in

if error != nil || data == nil {
print(“Client error!”)
return
}

guard let response = response as? HTTPURLResponse, (200…299).contains(response.statusCode) else {
print(“Server error!”)
return
}

guard let mime = response.mimeType, mime == “application/json” else {
print(“Wrong MIME type!”)
return
}

do {
let json = try JSONSerialization.jsonObject(with: data!, options: [])
print(json)
} catch {
print(“JSON error: \(error.localizedDescription)”)
}
}

task.resume()

Quick Tip: If you run the above code in Xcode Playground, it’s smart to use PlaygroundPage.current.needsIndefiniteExecution = true to enable infite execution. You can halt the playground again with PlaygroundPage.current.finishExecution(), for example when the async HTTP request returns. Don’t forget to import PlaygroundSupport.

Making POST Requests With URLSession

Another typical task of HTTP networking is uploading data to a webserver, and specifically making so-called POST requests. Instead of fetching data from a webserver, we’ll now send data back to that webserver.

A good example is logging into a website. Your username and password are sent to the webserver. And this webserver then checks your username and password against what’s stored in the database, and sends a response back. Similarly, when your Twitter app is used to create a new tweet, you send a POST request to the Twitter API with the tweet’s text.

Here’s what we’ll do:

  1. Set up the HTTP POST request with URLSession
  2. Set up the request headers and body
  3. Make the request with URLSessionUploadTask
  4. Print the returned response data

Set up the HTTP POST request with URLSession

Making POST requests with URLSession mostly comes down to configuring the request. We’ll do this by adding some data to a URLRequest object. Here’s how:

let session = URLSession.shared
let url = URL(string: “https://example.com/post”)!

var request = URLRequest(url: url)
request.httpMethod = “POST”
With the above code, we’re first creating a session constant with the shared URLSession instance, and we set up a URL object that refers to https://example.com/post.

Then, with that url object we create an instance of URLRequest and assign it to the request variable. On the last line we change the httpMethod to POST.

Set up the request headers and body

You can also use the URLRequest object to set HTTP Headers. A header is a special parameter that’s sent as part of the request, and it typically contains special information for the webserver or the web application. A good example is the Cookie header, that’s used to send cookie information back and forth.

Let’s add a few headers to the request:

request.setValue(“application/json”, forHTTPHeaderField: “Content-Type”)
request.setValue(“Powered by Swift!”, forHTTPHeaderField: “X-Powered-By”)
This is fairly easy. You can set a value of a given header field. The first header indicates that the request type is JSON, and the second header is just bogus.

The request needs a body. This is some data, typically text, that’s sent as part of the request message. In our case, it’ll be a JSON object that’s sent as a Data object.

We’ll start by creating a simple dictionary with some values:

let json = [
“username”: “zaphod42”,
“message”: “So long, thanks for all the fish!”
]
Then, we turn that dictionary into a Data object with:

let jsonData = try! JSONSerialization.data(withJSONObject: json, options: [])
The above step uses that same JSONSerialization class that we used before, but this time it does the exact opposite: turn an object into a Data object, that uses the JSON format.

It also uses try! to disable error handling, but keep in mind that in a production app you’ll need to handle errors appropriately.

You don’t have to use JSONSerialization for this; a great alternative is Codable!

Make the request with URLSessionUploadTask

We can now send the jsonData to the webserver with a URLSessionUploadTask instance. It’s similar to what we’ve done before, except that we’ll use the request and the data to create the task, instead of just the URL.

Here’s how:

let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
// Do something…
}

task.resume()
In the above code we’re creating an upload task with session.uploadTask(…, and provide request and jsonData as parameters. Instead of creating a simple data task, the above request will include those headers, body and URL we configured. As before, we can specify a completion handler, and the request is started once we call task.resume().

Print the returned response data

Inside the completion handler we should validate the response, and take appropriate action. For now, it’s OK to just read the response data with:

if let data = data, let dataString = String(data: data, encoding: .utf8) {
print(dataString)
}
The above code uses optional binding to turn the data optional into a String instance. And because the https://example.com/post URL doesn’t respond to POST requests, we get a nice error message in HTML format:





Meta Title – 404 – Not Found


H1- 404 – Not Found


And with the following code we can see that the HTTP status code is actually 404 Not Found. Like this:

if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
}
Awesome!

Quick Tip: If you want to debug network requests, I recommend Charles Proxy. And if you want to inspect or mock requests and webservice APIs, check out the excellent Paw app.

Further Reading

This little dance you do when making HTTP request is inherent to how the internet works. You request a resource from a webserver, validate the response, and take appropriate action.

On iOS, you can use URLSession to set up and make networking requests. It’s as straightforward as it gets, with practical objects such as HTTPURLResponse that give insight into what’s happening.


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts