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.
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
toJSONEncoding.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:
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.