April 01, 2016
Previously we set up some REST API Calls With Alamofire. While it’s a bit of overkill for those simple calls we can improve our code by using an Alamofire router. The router will put together the URL requests for us which will avoid having URL strings throughout our code. A router can also be used to apply headers, e.g., for including an OAuth token or other authorization header.
Updated to Swift 3.1 and Alamofire 4.4.
Yes, I see the date on this post. But well organized code is no joke. Be nice to your future self by making it easy to understand and work with your code. You’ll thank yourself later.
This tutorial is an excerpt from my iOS Apps with REST APIs guide.
Using a router with Alamofire is good practice since it helps keep our code organized. The router is responsible for creating the URL requests so that our API manager (or whatever makes the API calls) doesn’t need to do that along with all of the other responsibilities that it has. A good rule of thumb is to split out code if you can explain what each new object will be responsible for.
Our previous simple examples included calls to get, create, and delete todos using JSONPlaceholder. JSONPlaceholder is a fake online REST API for testing and prototyping. It’s like image placeholders but for web developers.
Previously when we made networking calls with Alamofire we used code like this:
// Get first todo
let todoEndpoint = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
.responseJSON { response in
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
}
if let todoJSON = response.result.value as? [String: Any] {
print("The todo is: " + todoJSON.description)
if let title = todoJSON["title"] as? String {
// to access a field:
print("The title is: " + title)
} else {
print("error parsing /todos/1")
}
}
}
// Create new todo
let todosEndpoint = "https://jsonplaceholder.typicode.com/todos"
let newTodo: [String: Any] = ["title": "My First Todo", "completed": false, "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 GET on /todos/1")
print(response.result.error!)
return
}
if let todoJSON = response.result.value as? [String: Any] {
// handle the results as JSON, without a bunch of nested if loops
print("The todo is: " + todoJSON.description)
}
}
// Delete first todo
let firstTodoEndpoint = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(firstTodoEndpoint, method: .delete)
.responseJSON { response in
if let error = response.result.error {
// got an error while deleting, need to handle it
print("error calling DELETE on /todos/1")
print(error)
} else {
print("deleted the todo item")
}
}
The bits that we’ll be changing look like Alamofire.request(...)
.Currently we’re providing the URL as a string and the HTTP method (unless it’s the default .get
). Instead of these two parameters Alamofire.request(...)
can also take a URLRequestConvertible
object. To be a URLRequestConvertible
object, we need to have an asURLRequest()
fucntion that returns the request we want to send. That’s what we’ll take advantage of to create our router.
Using an Alamofire Router
To start we’ll declare a router. It’ll be an enum with a case for each type of call we want to make. A convenient feature of Swift enums is that the cases can have arguments. For example, our .get
case can have an Int
argument so we can pass in the ID number of the todo that we want to get.
We’ll also need the base URL for our API so we can use it to build up the URLRequest
that we want to send:
import Foundation
import Alamofire
enum TodoRouter: URLRequestConvertible {
static let baseURLString = "https://jsonplaceholder.typicode.com/"
case get(Int)
case create([String: Any])
case delete(Int)
func asURLRequest() throws -> URLRequest {
// TODO: implement
}
}
If you’re working from scratch you’ll need to add Alamofire 4.0 to your project. CocoaPods is the easiest way to do that.
We’ll come back and implement the asURLRequest()
function in a bit. First let’s see how we need to change our existing calls to use the router.
For the get call all we need to do is to change:
let todoEndpoint = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
to
Alamofire.request(TodoRouter.get(1))
Don’t forget delete the line defining todoEndpoint
. All of the URL string handling is now done within the router.
The post call is similar. Change:
let todosEndpoint = "https://jsonplaceholder.typicode.com/todos"
let newTodo: [String: Any] = ["title": "My First Todo", "completed": false, "userId": 1]
Alamofire.request(todosEndpoint, method: .post, parameters: newTodo, encoding: JSONEncoding.default)
to
let newTodo = ["title": "My first todo", completed: false, "userId": 1]
Alamofire.request(TodoRouter.create(newTodo))
You can see there that the router has abstracted away the encoding as well as the endpoint from this function. The encoding is part of creating a URL request so it rightly belongs in the router, not the code making the API calls.
And for the delete call, change:
let firstTodoEndpoint = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(firstTodoEndpoint, method: .delete)
to
Alamofire.request(TodoRouter.Delete(1))
Now our calls are a bit easier to read. We could make them even clearer by naming the Router cases more descriptively, like Router.DeletePostWithID(1)
.
Generating the URL Requests
Within the router we need a function so that our calls like Router.delete(1)
give us a URLRequest
that Alamofire.Request()
knows how to use. The function where we do that is asURLRequest()
.
We’ve defined the Router as an enum with a case for each of our 3 calls. You’ll need a separate case if the URL, HTTP method (like GET or POST), or argument type is different. So within our router function we have three cases: .get
, .create
, and .delete
:
enum TodoRouter: URLRequestConvertible {
static let baseURLString: String = "https://jsonplaceholder.typicode.com/"
case get(Int)
case create([String: Any])
case delete(Int)
func asURLRequest() throws -> URLRequest {
// TODO: implement
}
}
We’ve also included the base URL since it’s the same in all of the calls. That’ll avoid the chance of having a typo in that part of the URL for one of the calls.
Within the asURLRequest()
function we’ll need a few elements that we’ll combine to create the url request: the HTTP method, any parameters to pass, and the URL.
Since we’re using an enum, we can use a switch statement to define the HTTP methods for each case:
var method: HTTPMethod {
switch self {
case .get:
return .get
case .create:
return .post
case .delete:
return .delete
}
}
We’ll need to add the parameters to post for the .create
case:
let params: ([String: Any]?) = {
switch self {
case .get, .delete:
return nil
case .create(let newTodo):
return (newTodo)
}
}()
Swift enums can have arguments, it’s called value binding. So when we use the .create
case we can pass in the parameters (in this case, the dictionary of data for the new todo). Then we can access it using:
case .myCase(let argument):
We’re also using it to for the .get
and .delete
cases to pass in the ID numbers for the gists. We’ll retrieve those when we need them to create the URL:
case .get(let number):
Now we can start to build up the URL request. First we’ll need the URL. We have the base URL added above so we can combine it with the relative path for each case to get the full URL:
let url: URL = {
// build up and return the URL for each endpoint
let relativePath: String?
switch self {
case .get(let number):
relativePath = "todos/\(number)"
case .create:
relativePath = "todos"
case .delete(let number):
relativePath = "todos/\(number)"
}
var url = URL(string: TodoRouter.baseURLString)!
if let relativePath = relativePath {
url = url.appendingPathComponent(relativePath)
}
return url
}()
We just set up the code to get the HTTP method, URL, and parameters for each case. Now we can put them together to create a URL request:
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
let encoding: ParameterEncoding = {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}()
return try encoding.encode(urlRequest, with: params)
First we create a mutable request using the URL. It’s mutable because we declared it with var
, not let
. That’s necessary so we can set the httpMethod
on the next line. Then we encode any parameters and add them to the request. This web service uses JSON, like most these days, so we’re using JSONEncoding.default
. Generally GET
requests want the parameters in the URL like https://jsonplaceholder.typicode.com/comments?postId=1
. So we in that case we use URLEncoding.default
. ALways check your API docs to see what kind of encoding is expected for each call.
Finally, we return the request. Here’s it all together:
import Foundation
import Alamofire
enum TodoRouter: URLRequestConvertible {
static let baseURLString = "https://jsonplaceholder.typicode.com/"
case get(Int)
case create([String: Any])
case delete(Int)
func asURLRequest() throws -> URLRequest {
var method: HTTPMethod {
switch self {
case .get:
return .get
case .create:
return .post
case .delete:
return .delete
}
}
let params: ([String: Any]?) = {
switch self {
case .get, .delete:
return nil
case .create(let newTodo):
return (newTodo)
}
}()
let url: URL = {
// build up and return the URL for each endpoint
let relativePath: String?
switch self {
case .get(let number):
relativePath = "todos/\(number)"
case .create:
relativePath = "todos"
case .delete(let number):
relativePath = "todos/\(number)"
}
var url = URL(string: TodoRouter.baseURLString)!
if let relativePath = relativePath {
url = url.appendingPathComponent(relativePath)
}
return url
}()
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
let encoding: ParameterEncoding = {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}()
return try encoding.encode(urlRequest, with: params)
}
}
Here’s the example code on GitHub.
And our demo view controller that uses it:
import UIKit
import Alamofire
class ViewController: UIViewController {
func getFirstPost() {
let todoEndpoint = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
.responseJSON { response in
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
}
if let todoJSON = response.result.value as? [String: Any] {
print("The todo is: " + todoJSON.description)
if let title = todoJSON["title"] as? String {
// to access a field:
print("The title is: " + title)
} else {
print("error parsing /todos/1")
}
}
}
}
func createPost() {
let todosEndpoint = "https://jsonplaceholder.typicode.com/todos"
let newTodo: [String: Any] = ["title": "My First Todo", "completed": false, "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 GET on /todos/1")
print(response.result.error!)
return
}
if let todoJSON = response.result.value as? [String: Any] {
// handle the results as JSON, without a bunch of nested if loops
print("The todo is: " + todoJSON.description)
}
}
}
func deleteFirstPost() {
let firstTodoEndpoint = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(firstTodoEndpoint, method: .delete)
.responseJSON { response in
if let error = response.result.error {
// got an error while deleting, need to handle it
print("error calling DELETE on /todos/1")
print(error)
} else {
print("deleted the todo item")
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// pick which method you want to test here
// comment out the other two
getFirstPost()
//createPost()
//deleteFirstPost()
}
}
Save and test out our code. Un-comment each of the function calls in viewDidLoad
to test out each of the API calls. The console log should display the post titles or success messages without showing any errors.
We’ve created a simple Alamofire router that you can adapt to your API calls :)
Here’s the example code on GitHub.
This tutorial is an excerpt from my iOS Apps with REST APIs guide. It’ll teach you how to build Swift apps that get their data from web services without hundreds of pages about flatmap and Core Whatever. iOS Apps with REST APIs guide is the book I wish I had on my first iOS contract when I was struggling to figure out how to get the API data showing up in the app I was building.
Start Building iOS Apps with REST APIs now for $29(Buying for a group? Grab the discounted Team License from LeanPub.)