In this 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.
Ready? Let’s go.
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:
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.URLSession
is used to create URLSessionTask
instances, which can fetch and return data to your app, and download and upload files to webservices.URLSessionConfiguration
object. This configuration manages caching, cookies, connectivity and credentials.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.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 URLSessionTask
s, based on the parameters you put in.
With URLSession
, we differentiate between three kinds of tasks:
URLSessionDataTask
, by using NSData
objects. They’re the most common for webservice requests, for example when working with JSON.URLSessionUploadTask
. They’re similar to data tasks, but URLSessionUploadTask
instances can also upload data in the background (or when an app is suspended).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.
Let’s fetch some data with URLSession
! Here’s what we’re going to do:
URLSession
URLSessionDataTask
JSON
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.
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:
data
object, of type Data
, to check out what data we got back from the webserver, such as the JSON we’ve requestedresponse
object, of type URLResponse
, can tell us more about the request’s response, such as its length, encoding, HTTP status code, return header fields, etceteraerror
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.
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:
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.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.error
? It’s nil
. Fortunately, no errors were passed to the completion handler. That doesn’t mean the request is OK, though!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:
error
object.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:
throw
and use promises to deal with any thrown or passed errors in the chain’s .catch
clauseThen, 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:
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().”response
of type URLResponse
to HTTPURLResponse
. This downcast ensures we can use the statusCode
property on the response, which is only part of HTTPURLResponse
.(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.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.
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:
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.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
.
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:
URLSession
URLSessionUploadTask
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.
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!
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()
.
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:
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>404 - Not Found</title>
</head>
<body>
<h1>404 - Not Found</h1>
</body>
</html>
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.
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.
Want to learn more? Check out these resources: