February 16, 2015 - Updated: November 10, 2016 - Swift 3.0
Last time we looked at the quick & dirty way to get access REST APIs in iOS. For simple cases, like a URL shortener, dataTask(with request: completionHandler:)
works just fine. But these days lots of apps have tons of web service calls that are just begging for better handling, like a higher level of abstraction, concise syntax, simpler streaming, pause/resume, progress indicators, …
In Objective-C, this was a job for AFNetworking. In Swift, Alamofire is our option for elegance.
This tutorial has been updated for Swift 3.0.1 and Alamofire 4.0.
Just like last time, we’ll use the super handy JSONPlaceholder as our API: “JSONPlaceholder is a fake online REST API for testing and prototyping. It’s like image placeholders but for web developers.”.
Here’s our quick & dirty GET request from last time where we grabbed the first todo and printed out it’s title:
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
do {
guard let todo = try JSONSerialization.jsonObject(with: responseData, options: [])
as? [String: Any] else {
print("error trying to convert data to JSON")
return
}
// now we have the todo
// let's just print it to prove we can access it
print("The todo is: " + todo.description)
// the todo object is a dictionary
// so we just access the title using the "title" key
// so check for a title and print it if we have one
guard let todoTitle = todo["title"] as? String else {
print("Could not get todo title from JSON")
return
}
print("The title is: " + todoTitle)
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
Which is an awful lot of code for what we’re doing (but far less than the days of thousands of lines of code generated from WSDL that crashed Xcode just by scrolling the file, thank FSM). No authentication and just enough error checking to get by.
So how about using Alamofire? First add Alamofire v4.0 to your project and import it in the file where you’re working:
import UIKit
import Alamofire
class ViewController: UIViewController {
// ...
}
Then set up the request:
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
.responseJSON { response in
// ...
}
We’re telling Alamofire to set up & send an asynchronous request to todoEndpoint (without the ugly call to URL
to wrap up the string). We don’t have to explicitly say it’s a GET request since that’s the default HTTP method. If we wanted to specify the HTTP method then we’d use a member of Alamofire’s HTTPMethod
enum, which includes .get
, .post
, .patch
, .options
, .delete
, etc. We can add the method when creating the request to make it explicit:
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint, method: .get)
.responseJSON { response in
// ...
}
Then we get the data (asynchronously) as JSON in the .responseJSON
. We could also use .response
(for an NSHTTPURLResponse
), .responsePropertyList
, or .responseString
(for a string). We can even chain multiple .responseX
methods for debugging:
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
.responseJSON { response in
// handle JSON
}
.responseString { response in
if let error = response.result.error {
print(error)
}
if let value = response.result.value {
print(value)
}
}
That’s neat but right now we just want to get the todo’s title from the JSON. We’ll make the request then handle it with .responseJSON
. Like last time we need to do some error checking:
- Check for an error returned by the API call
- If no error, see if we got any JSON results
- Check for an error in the JSON transformation
- If no error, access the todo object in the JSON and print out the title
.responseJSON
takes care of some of the boilerplate we had to write earlier. It makes sure we got response data then calls JSONSerialization.jsonObject
for us. So we can just check that we got the JSON object that we expected. In this case that’s a dictionary so we’ll use as? [String: Any]
in our guard statement.
When using the .responseX
handlers in Alamofire, the value that you want will be be in response.result.value
(e.g., a string for .responseString
or the JSON in .responseJSON
). If that value can’t be parsed or there’s a problem with the call then you’ll get an error in response.result.error
.
So to check for errors then get the JSON if there are no errors:
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
.responseJSON { response in
guard let json = response.result.value as? [String: Any] else {
print("didn't get todo object as JSON from API")
print("Error: \(response.result.error)")
return
}
print(json)
}
You can use response.result.isSuccess
if you just need to know whether the call succeeded or failed:
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
.responseJSON { response in
guard response.result.isSuccess else {
// handle failure
return
}
// handle success
}
There’s another possibility that our current code doesn’t consider well: what if we didn’t get an error but we also didn’t get any JSON or the JSON isn’t a dictionary? JSON top-level objects can be arrays as well as dictionaries. Let’s split up the code that checks that we got the JSON we expected and the code that checks for errors in to two separate guard statements. Some APIs will return error messages as JSON that’s a different format than what you requested. In those cases we need to differentiate between not getting anything and not getting the JSON that we expected:
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
.responseJSON { response in
// check for errors
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling GET on /todos/1")
print(response.result.error!)
return
}
// make sure we got some JSON since that's what we expect
guard let json = response.result.value as? [String: Any] else {
print("didn't get todo object as JSON from API")
print("Error: \(response.result.error)")
return
}
print(json)
}
Finally we can extract the title from the JSON just like we did in the previous section:
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
.responseJSON { response in
// check for errors
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling GET on /todos/1")
print(response.result.error!)
return
}
// make sure we got some JSON since that's what we expect
guard let json = response.result.value as? [String: Any] else {
print("didn't get todo object as JSON from API")
print("Error: \(response.result.error)")
return
}
// get and print the title
guard let todoTitle = json["title"] as? String else {
print("Could not get todo title from JSON")
return
}
print("The title is: " + todoTitle)
}
To POST, we just need to change the HTTP method and provide the todo item as JSON data:
let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos"
let newTodo: [String: Any] = ["title": "My First Post", "completed": 0, "userId": 1]
Alamofire.request(todosEndpoint, method: .post, parameters: newTodo,
encoding: JSONEncoding.default)
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling POST on /todos/1")
print(response.result.error!)
return
}
// make sure we got some JSON since that's what we expect
guard let json = response.result.value as? [String: Any] else {
print("didn't get todo object as JSON from API")
print("Error: \(response.result.error)")
return
}
// get and print the title
guard let todoTitle = json["title"] as? String else {
print("Could not get todo title from JSON")
return
}
print("The title is: " + todoTitle)
}
And DELETE is nice and compact:
let firstTodoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(firstTodoEndpoint, method: .delete)
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling DELETE on /todos/1")
print(response.result.error!)
return
}
print("DELETE ok")
}
Grab the example code on GitHub.
So that’s one step better on our journey to nice, clean REST API calls. But we’re still interacting with un-typed JSON which can easily lead us to errors. An improvement would be to use a class for our Todo objects. To get that article and future tips sent directly to your inbox, sign up below.