Updating Alamofire Calls to Swift 3.0
October 05, 2016 - Swift 3.0

Got a Swift project using Alamofire that needs to be migrated to Swift 3.0? Then you’ve probably figured out just how painful it can be if you try to just run the Xcode migration tool and figure out all of the errors. Changes to both Swift and Alamofire can make it tough to know just how to update those calls. So let’s look at a few common types of the Alamofire calls in Swift 2.2 and see how we can update them for Swift 3.0.

Then we’ll figure out how to create strongly typed objects in our code so that we’re not stuck passing JSON around everywhere.

Programmer hands on MacBook Pro keyboard
(image by #WOCinTech Chat)

This tutorial uses Swift 3.0 and Alamofire 4.0.

If you haven’t yet, the first thing you need to do is updating your Podfile. Swift 3.0 requires Alamofire 4.0. Update your Podfile to require the new version of Alamofire:


platform :ios, '9.0'

target 'MyTarget' do
  use_frameworks!

  pod 'Alamofire', '~> 4.0'
end

Make sure you use your target’s name instead of MyTarget. If you’re not sure how to find that, copy your Podfile somewhere else then delete the copy in your project’s folder. Run pod init and it’ll generate a Podfile that you can fill it. Then copy your pods back in from the original file.

Then run pod update to get the new version of Alamofire added to your project.

Migration Guide

Alamofire has provided a migration guide to make it easier to upgrade to 4.0. There’s a section in there on Breaking API Changes that should cover most of the problems we find.

There’s even a section showing examples of updating to the new syntax for requests:


// Alamofire 3
Alamofire.request(.GET, urlString).response { request, response, data, error in
    print(request)
    print(response)
    print(data)
    print(error)
}

// Alamofire 4
Alamofire.request(urlString).response { response in // method defaults to `.get`
    debugPrint(response)
}

GET Request

Here’s a simple Swift 2.2 example of making a GET request:


let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(.GET, todoEndpoint)
  .responseString { response in
    // print response as string for debugging, testing, etc.
    print(response.result.value)
    print(response.result.error)
}

Based on the migration guide, we can update the syntax for the request call by removing the .GET parameter since that’s now the default:


let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(todoEndpoint)
  .responseString { response in
    // print response as string for debugging, testing, etc.
    print(response.result.value)
    print(response.result.error)
}

POST Request

To make a POST request, we need to figure out how to include the HTTP method and the parameters. Here’s what the migration guide shows for the new syntax:


Alamofire.request(urlString, method: .get, parameters: parameters, encoding: JSONEncoding.default)

A Swift 2.2 POST request would look like:


let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos"
let newTodo = ["title": "Frist Psot", "completed": 0, "userId": 1]
Alamofire.request(.POST, todosEndpoint, parameters: newTodo, encoding: .JSON)
  .responseJSON { response in
    debugPrint(response)
}

So we need to:

  • Reorder the arguments
  • Change .POST to .post
  • Change .JSON to JSONEncoding.default (or whatever encoding we should be sending)

Here’s our updated code, according to what the Alamofire migration guide says we need to change:


let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos"
let newTodo = ["title": "Frist Psot", "completed": 0, "userId": 1]
Alamofire.request(todosEndpoint, method: .post, parameters: newTodo, encoding: JSONEncoding.default)
  .responseJSON { response in
    debugPrint(response)
}

But if we try to run that we’ll get an error from the compiler:

Error about missing type for dictionary

The wording is a little confusing but at least the suggestion is pretty clear. We need to tell the compiler what the type of newTodo should be:


let newTodo: [String: Any] = ["title": "Frist Psot", "completed": 0, "userId": 1]

Change that then save & run to make sure it works.

Other HTTP Methods

Other HTTP methods are pretty similar to POST calls. Here’s a delete call in Swift 2.2:


let firstTodoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
Alamofire.request(.DELETE, firstTodoEndpoint)
  .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("delete ok")
    }
}

To update it, move the .DELETE parameter to after the endpoint, change it to .delete, and add method: to it:


let firstTodoEndpoint: String = "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("delete ok")
    }
}

Turning JSON into Objects

What about handling the results? We’ll want to turn the resulting JSON into Todo objects. Let’s figure out how to do that in Alamofire 4.0.

Todo class

Here’s a Swift 2.2 class for our Todo model objects:


class Todo {
  var title: String?
  var id: Int?
  var userId: Int?
  var completed: Int?
  
  required init?(aTitle: String?, anId: Int?, aUserId: Int?, aCompletedStatus: Int?) {
    self.title = aTitle
    self.id = anId
    self.userId = aUserId
    self.completed = aCompletedStatus
  }

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

Nothing there needs to be updated for Swift 3.0 :)

Response Serializers

In previous tutorials we used custom response serializers to extract the objects and arrays from the JSON. While that was a neat generic solution that was easy to reuse for multiple classes, it required explicitly calling NSJSONSerialization and a bunch of other steps:


extension Alamofire.Request {
  public func responseObject(completionHandler: Response -> Void) -> Self {
    let responseSerializer = ResponseSerializer { request, response, data, error in
      guard error == nil else {
        return .Failure(error!)
      }
      guard let responseData = data else {
        let failureReason = "Array could not be serialized because input data was nil."
        let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
        return .Failure(error)
      }
      
      let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
      let result = JSONResponseSerializer.serializeResponse(request, response, responseData, error)
      
      if result.isSuccess {
        if let value = result.value {
          let json = SwiftyJSON.JSON(value)
          if let newObject = T(json: json) {
            return .Success(newObject)
          }
        }
      }

      let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: "JSON could not be converted to object")
      return .Failure(error)
    }
    
    return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
  }

  ...
}

It’s simpler to use Alamofire’s existing .responseJSON serializer and not bother with custom response serializers. We’ll still need a few error checks but it won’t be quite as complex as above.

Here’s what our Swift 2.2 function to get a single Todo looked like:


class Todo: ResponseJSONObjectSerializable {
  ...
  class func todoByID(id: Int, completionHandler: (Result<Todo, NSError>) -> Void) {
    Alamofire.request(.GET, Todo.endpointForID(id))
      .responseObject { response in
        completionHandler(response.result)
    }
  }
}

We’ll remove ResponseJSONObjectSerializable (since that was only needed for .responseObject). Then change it to use .responseJSON instead of .responseObject:


class Todo {
  ...
  class func todoByID(id: Int, completionHandler: (Result<Todo, NSError>) -> Void) {
    Alamofire.request(.GET, Todo.endpointForID(id))
      .responseJSON { response in
        // TODO: create Todo from JSON and
        // give it to the completion handler
    }
  }
}

But we need to figure out what to do when we get the response as JSON. Well, we previously created an initializer to create a Todo from JSON:


import SwiftyJSON

class Todo {
  ...

  required init?(json: SwiftyJSON.JSON) {
    self.title = json["title"].string
    self.id = json["id"].int
    self.userId = json["userId"].int
    self.completed = json["completed"].int
  }

  ...
}

We had used SwiftyJSON since parsing JSON in Swift used to be pretty ugly. It’s gotten much better in recent versions as the language has matured so let’s switch to native Swift JSON parsing using guard and as?:


class Todo {
  ...

  convenience init?(json: [String: Any]) {
    guard let title = json["title"] as? String,
      let idValue = json["id"] as? Int,
      let userId = json["userId"] as? Int,
      let completed = json["completed"] as? Bool
      else {
        return nil
    }
    
    self.init(title: title, id: idValue, userId: userId, completedStatus: completed)
  }

  ...
}

The JSON that we’ll get from .responseJSON for this API call will be a dictionary: [String: Any]. Then we can extract each element from the JSON like json["title"] and make sure it’s the right type using as?.

By placing the parsing of the required parameters in the guard statement we can make sure that the API gives us all of the values that we need before creating the object.

Now we can use that function to create a Todo from the JSON we get from the network call. When doing that we need to make sure that:

  • We didn’t get an error when making the API call
  • We got a JSON dictionary in the response
  • The initializer finds all of the properties that it needs in the JSON dictionary

So let’s do that:


.responseJSON { response in
  // check if responseJSON already has an error
  // e.g., no network connection
  guard response.result.error == nil else {
    print(response.result.error!)
    completionHandler(.failure(response.result.error!))
    return
  }
  
  // make sure we got JSON and it's a dictionary
  guard let json = response.result.value as? [String: AnyObject] else {
    print("didn't get todo object as JSON from API")
    completionHandler(.failure(BackendError.objectSerialization(reason: "Did not get JSON dictionary in response")))
    return
  }
  
  // create Todo from JSON, make sure it's not nil
  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. If we found additional types of errors that we need to handle we could add them to this enumeration later:


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

If you’ll be getting the same object back from a few different API calls then you can move that functionality in to a reusable function. The response object that Alamofire hands us in the .responseJSON completion handler is a DataResponse<Any>. You can find that out by command-clicking on response. We don’t need the whole response, just the result bit (with either an error or the JSON we asked for). So that’ll be the type of the input argument for this function:


private class func todoFrom(result: Alamofire.Result<Any>) -> Result<Todo> {
  guard result.error == nil else {
    // got an error in getting the data, need to handle it
    print(result.error!)
    return .failure(result.error!)
  }
  
  // make sure we got JSON and it's a dictionary
  guard let json = result.value as? [String: AnyObject] 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)
}

And call it within .responseJSON:


class func todoByID(id: Int, completionHandler: @escaping (Result<Todo>) -> Void) {
  let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/\(id)"
  Alamofire.request(todoEndpoint)
    .responseJSON { response in
      let todoResult = Todo.todoFrom(result: response.result)
      completionHandler(todoResult)
  }
}

To reuse it, just set up another call and use the same .responseJSON handler:


func save(completionHandler: @escaping (Result<Todo>) -> Void) {
  let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos"
  let fields = self.toJSON()
  Alamofire.request(todosEndpoint, method: .post, parameters:   fields, encoding: JSONEncoding.default)
    .responseJSON { response in
      let todoResult = Todo.todoFrom(result: response.result)
      completionHandler(todoResult)
  }
}

That requires a handy function to turn a Todo into a JSON dictionary:


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
}

Handling a JSON Array

If you have to handle getting back a collection of objects from the JSON, that can be handled similarly. The difference is that you’ll transform each item in the JSON array into one of your objects, after making the error checks like we did above:


class func getTodos(completionHandler: @escaping (Result<[Todo]>) -> Void) {
  let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos/"
  Alamofire.request(todosEndpoint)
    .responseJSON { response in
      guard response.result.error == nil else {
        // got an error in getting the data, need to handle it
        print(response.result.error!)
        completionHandler(.failure(response.result.error!))
        return
      }
      
      // make sure we got JSON and it's an array of dictionaries
      guard let json = response.result.value as? [[String: AnyObject]] else {
        print("didn't get todo objects as JSON from API")
        completionHandler(.failure(BackendError.objectSerialization(reason: "Did not get JSON array in response")))
        return
      }
      
      // turn each item in JSON in to Todo object
      var todos:[Todo] = []
      for element in json {
        if let todoResult = Todo(json: element) {
          todos.append(todoResult)
        }
      }
      completionHandler(.success(todos))
  }
}

Compared to the previous example there are 2 big differences:

We check for an array of JSON dictionaries, not just a single dictionary:


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

And we loop through the elements in the JSON array to transform them in to Todos before returning them:


var todos:[Todo] = []
for element in json {
  if let todo = Todo(json: element) {
    todos.append(todo)
  }
}
completionHandler(.success(todos))

If you want to be extra Swift-y, you can do the same thing with .flatMap:


let todos = json.flatMap { Todo(json: $0) }
completionHandler(.success(todos))

.flatMap can take an array, apply a transformation to each element (in this case, calling the Todo JSON initializer), and return the resulting array. Using .flatMap instead of just .map means that any nil elements will get removed, so we don’t need to include if let todo = Todo(json: element) { ... }. (You can also use .flatMap to work with nested arrays but we don’t need that right now.)

And That’s All

That’s how you update a existing simple Alamofire networking calls to Swift 3.0 and Alamofire 4.0. Over the next while I’ll be working through how to update more networking code to Swift 3.0, including handling headers and authentication. If there’s something specific you want to see, leave a comment or email me.

If you’d like more Swift tutorials on topics like this one, sign up below to get them sent directly to your inbox.

Want more Swift tutorials like this one?

Sign up to get the latest GrokSwift tutorials and information about GrokSwift books sent straight to your inbox

Other Posts You Might Like