Strongly-Typed GET and POST Calls With Alamofire
February 18, 2015 - Updated: November 16, 2016 - Swift 3.0

Last time we used Alamofire to make some REST requests to a web service. But we’re still passing JSON around in our app. Today let’s improve that up by building a higher layer of abstraction by mapping the JSON to a strongly typed class. That’ll keep our code better organized so we aren’t struggling to keep too many details in our minds at once.

First, we’ll need a class to represent the Todo objects we’re dealing with. Create a new class in its own file to represent the Todo objects. It will have a few properties, an initializer to create new Todo objects, and a description function to print out all of the properties, which is handy for debugging:

This tutorial has been updated for Swift 3.0.1 and Alamofire 4.0.


class Todo {
  var title: String
  var id: Int?
  var userId: Int
  var completed: Bool
  
  required init?(title: String, id: Int?, userId: Int, completedStatus: Bool) {
    self.title = title
    self.id = id
    self.userId = userId
    self.completed = completedStatus
  }

  func description() -> String {
    return "ID: \(self.id), " +
      "User ID: \(self.userId), " +
      "Title: \(self.title)\n" +
      "Completed: \(self.completed)\n"
  }
}

The id is optional because when we create a new Todo object within the app it won’t have an ID until it’s been saved to the server.

So we’ve got a Todo class with some pretty obvious but optional fields. For our GET and POST requests we’ll need to access the endpoints for all todos and a single todo:


class Todo {
  ...

  // MARK: URLs
  class func endpointForID(_ id: Int) -> String {
    return "https://jsonplaceholder.typicode.com/todos/\(id)"
  }
  class func endpointForTodos() -> String {
    return "https://jsonplaceholder.typicode.com/todos/"
  }
}

When setting up API calls you can work top down or bottom up. Either way works fine but I usually prefer to work from the top down: starting with the high level code and working down to the lower level details. There are several ways to implement the low level code. Working from the top down lets us design how we’ll interact with the lower levels. Then we can implement the bottom level to fit the easy to understand way that we’ve implemented the high level code.

So we’ll start by implementing how we’d like to have the calls look in our view controller. Then we’ll dig in deeper to figure out how to make them work.

First, we want to be able to GET a todo from an ID number. We can do this in the View Controller’s viewWillAppear function. We’ll add a simple completion handler that checks that the call succeeded by checking for errors or not getting a todo:


// MARK: Get Todo #1
Todo.todoByID(1) { result in
  if let error = result.error {
    // got an error in getting the data, need to handle it
    print("error calling POST on /todos/")
    print(error)
    return
  }
  guard let todo = result.value else {
    print("error calling POST on /todos/ - result is nil")
    return
  }
  // success!
  print(todo.description())
  print(todo.title)
}

We’re using a completion handler so we can make the API calls asynchronously. Notice that there are no references to URLs or requests or JSON in the code above. It deals entirely with Todos, not the underlying levels of abstraction.

Before digging into the implemention for todoByID, let’s also figure out how we’ll want to call other API methods. There are probably some similarities that will mean we can share code between these functions.

We’ll also want to be able to create Todos by sending them to the server:


// MARK: Create new todo
guard let newTodo = Todo(title: "My first todo",
  id: nil,
  userId: 1,
  completedStatus: true) else {
  print("error: newTodo isn't a Todo")
  return
}
newTodo.save { result in
  guard result.error == nil else {
    // got an error in getting the data, need to handle it
    print("error calling POST on /todos/")
    print(result.error!)
    return
  }
  guard let todo = result.value else {
    print("error calling POST on /todos/. result is nil")
    return
  }
  // success!
  print(todo.description())
  print(todo.title)
}

We’ve separated creating a new Todo object locally from saving it on the server: Todo(...) vs newTodo.save(...). We’re leaving the ID number blank on creation since that will be assigned by the server.

Let’s set up some Alamofire requests and see how we can interface them to those Todo calls. This code will end up inside of our newTodo.save and Todo.todoByID functions.

First the GET request (using our handy-dandy URL endpoint class funcs):


Alamofire.request(Todo.endpointForID(id))
  .responseJSON { response in
    // ...
  }

Now within that function we need to take the JSON that Alamofire gets for us and turn it into a Todo object. We’ll have to extract each property from the JSON and set it on our new todo object. Since we’ll be creating a new todo object, we can write a new initializer in the Todo class to encapsulate this conversion.

To keep our code organized, we can use an extension to hold the networking related code for the Todo object.

Extensions add new functionality to an existing class, structure, enumeration, or protocol type. - Apple Documentation on Extensions in Swift

Create a new file for this extension and name it Todo+Networking.swift. To extend the current Todo class, start with extension Todo:


import Foundation
import Alamofire

extension Todo {
  // ...
}

Since we’ll be using Alamofire to make networking requests we need to import it in that file.

Then we can add the new initializer that will create a Todo object from JSON. The JSON representation of a Todo is a dictionary, so its type is [String: Any].


extension Todo {
  convenience init?(json: [String: Any]) {
    // ...
  }
}

To parse the JSON, first we need to extract each parameter from the JSON. Here’s what the JSON looks like for a single Todo:


{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

To parse out each property from the JSON, use the title as a key to get it from the dictionary:


json["title"]

Then try to cast it to the type that we expect:


let title = json["title"] as? String

When parsing JSON for an initializer, we can first check that we can get all of the required properties using a guard statement. If we don’t find everything that’s needed then we can return nil to indicate that we couldn’t create our object:


extension Todo {
  convenience init?(json: [String: Any]) {
    guard let title = json["title"] as? String,
      let userId = json["userId"] as? Int,
      let completed = json["completed"] as? Bool
      else {
        return nil
    }

    // ...
  }
}

Since the ID property is optional, we don’t need to require it in the guard statement but we do need to parse it before creating a Todo:


extension Todo {
  convenience init?(json: [String: Any]) {
    guard let title = json["title"] as? String,
      let userId = json["userId"] as? Int,
      let completed = json["completed"] as? Bool
      else {
        return nil
    }

    let idValue = json["id"] as? Int
    
    // ...
  }
}

And finally we can use the existing initializer to create a Todo:


extension Todo {
  convenience init?(json: [String: Any]) {
    guard let title = json["title"] as? String,      
      let userId = json["userId"] as? Int,
      let completed = json["completed"] as? Bool
      else {
        return nil
    }

    let idValue = json["id"] as? Int
    
    self.init(title: title, id: idValue, userId: userId, completedStatus: completed)
  }
}

Now we can use that initializer to create our Todo.byID() function. Since this code is also networking related, we’ll put it in the Todo+Networking extension:


import Alamofire

extension Todo {
  // ..

  class func todoByID(_ id: Int, completionHandler: @escaping (Result<Todo>) -> Void) {
    Alamofire.request(Todo.endpointForID(id))
      .responseJSON { response in
         // ...
      }
  }
}

The completion handler has a single argument: Result<Todo>. Alamofire defines the Result struct. When working with Alamofire we’ll also encounter the Response struct. It’s a handy way to pack up a bunch of bits where we used to have to use a big tuple like (URLRequest?, NSHTTPURLResponse?, Result<Todo>). The Result struct packs up the result (our Todo object and/or an error).

Think of the Response and Result structs as little packages of data that make up what we get from fetching the response and serializing it into the format that we want. It’s kind of like when you buy something in person. You hand over your payment and you get back a few things: your purchase, your change, a receipt, or maybe an error message like “card declined” or “you’re short 12 cents”. All of these things make up the response to your purchase.

You could also think of the purchase and/or error message as the result of your transaction: whether it’s a success or a failure.

Alamofire’s structs are similar: Result has .success and .failure cases and might have what you asked for or an error. Response is higher level: it packs up the Result along with all of the other info from the transaction like your original request and the raw response.

Within the .responseJSON completion handler, we get a Response object. We need to handle that response, check for errors and return either an error or a Todo object. Instead of having two optional parameters for our completion handler:


(Todo?, Error?) -> Void

We can use a single Result parameter where we expect the object returned to be a Todo:


(Result<Todo>) -> Void

Now let’s fill in that function. Like with the custom initializer, we’ll can check for errors using guard statements. There are a few possible sources of errors:

Errors reported by Alamofire when it makes the request or serializes the JSON, e.g., 404 response, no network connection, response isn’t JSON. Those errors will be returned in response.result.error if there are any.


guard response.result.error == nil else { ... }

The JSON is valid but it’s not in the format we expect: i.e., it’s not a dictionary:


guard let json = response.result.value as? [String: Any] else { ... }

Finally, we can have an error if we can’t create a Todo object from the JSON dictionary.

If at any point we find an error, we’ll give it to the completion handler as a .failure case and return:


completionHandler(.failure(error))
return

And if we succeed, we’ll call the completion handler with the new Todo:


completionHandler(.success(todo))

So let’s do that:


class func todoByID(_ id: Int, completionHandler: @escaping (Result<Todo>) -> Void) {
  Alamofire.request(Todo.endpointForID(id))
    .responseJSON { response in
      // check for errors from responseJSON      
      guard response.result.error == nil else {
        // got an error in getting the data, need to handle it
        print("error calling GET on /todos/\(id)")
        print(response.result.error!)
        completionHandler(.failure(response.result.error!))
        return
      }
      
      // make sure we got a JSON dictionary
      guard let json = response.result.value as? [String: Any] else {
        print("didn't get todo object as JSON from API")
        completionHandler(.failure(BackendError.objectSerialization(reason:
          "Did not get JSON dictionary in response")))
        return
      }
      
      // turn JSON in to Todo object
      guard let todo = Todo(json: json) else {
        completionHandler(.failure(BackendError.objectSerialization(reason:
          "Could not create Todo object from JSON")))
        return
      }
      completionHandler(.success(todo))
    }
}

To be able to use that BackendError type we need to declare it:


enum BackendError: Error {
  case objectSerialization(reason: String)
}

And that’s it for the GET call. We can run our nice pretty Todo.todoByID(1) call now.

But, of course, there are always more requirements. We said we’d implement the POST call to save new Todos too.

In our Todo class, we’ll need a method to turn a Todo into a dictionary with String keys (which we’ll call json for convenience). We’ll put it in our Todo+Networking extension:


func toJSON() -> [String: Any] {
  var json = [String: Any]()
  json["title"] = title
  if let id = id {
    json["id"] = id
  }
  json["userId"] = userId
  json["completed"] = completed
  return json
}

For most of the properties, we can just add them to the String: Any dictionary:


json["title"] = title

Since the id property is optional, we need to make sure it has a value before adding it to the JSON:


if let id = id {
  json["id"] = id
}

To finish implementing the save() function for Todos:


// POST / Create
func save(completionHandler: @escaping (Result<Todo>) -> Void) {
  let fields = self.toJSON()
  Alamofire.request(Todo.endpointForTodos(),
                      method: .post,
                      parameters: fields,
                      encoding: JSONEncoding.default,
                      headers: nil)
    .responseJSON { response in
      // handle response
  }
}

To handle that response, we want to do exactly the same as we did for creating a new Todo: check for errors and parse the Todo from the JSON. So let’s refactor that functionality into its own function so we can reuse it:


private class func todoFromResponse(response: DataResponse<Any>) -> Result<Todo> {
  guard response.result.error == nil else {
    // got an error in getting the data, need to handle it
    print(response.result.error!)
    return .failure(response.result.error!)
  }
  
  // make sure we got JSON and it's a dictionary
  guard let json = response.result.value as? [String: Any] else {
    print("didn't get todo object as JSON from API")
    return .failure(BackendError.objectSerialization(reason:
      "Did not get JSON dictionary in response"))
  }
  
  // turn JSON in to Todo object
  guard let todo = Todo(json: json) else {
    return .failure(BackendError.objectSerialization(reason:
      "Could not create Todo object from JSON"))
  }
  return .success(todo)
}

Since it doesn’t have to be asynchronous, we can just return the Result<Todo> instead of having to call the completion handler. We just return the .success or .failure case and let the calling function pass it to the completion handler:


class func todoByID(_ id: Int, completionHandler: @escaping (Result<Todo>) -> Void) {
  Alamofire.request(Todo.endpointForID(id))
    .responseJSON { response in
      let result = Todo.todoFromResponse(response: response)
      completionHandler(result)
  }
}

And we can reuse this function in the save function:


func save(completionHandler: @escaping (Result<Todo>) -> Void) {
  let fields = self.toJSON()
  Alamofire.request(Todo.endpointForTodos(),
                    method: .post,
                    parameters: fields,
                    encoding: JSONEncoding.default,
                    headers: nil)
    .responseJSON { response in
      let result = Todo.todoFromResponse(response: response)
      completionHandler(result)
  }
}

And That’s All

And that’s it! Now we can run our nice pretty calls to retrieve and save todos that we set up at the start. Even better, the caller of those functions no longer has any knowledge of how the todos are getting retrieved & saved. We could completely remove Alamofire to switch to RESTKit or a completely different implementation for the API calls without having to touch the view controller code at all :)

Here’s the final code: GitHub Gist

Making your first Swift app that uses a web service can be overwhelming

It seems like every time you try to figure it out you just add more things to learn to your list: REST, Alamofire, parsing JSON, OAuth, App Transport Security, setting headers, …

But it doesn’t have to be confusing. In this free 5-part course, we’ll work through what you really need to know, one step at a time, building our skills as we go. (And get free Swift tutorials, sent straight to your inbox.)

iOS Apps with REST APIs Cover

With iOS Apps with REST APIs ebook I’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 ebook 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.

Other Posts You Might Like