Show more

 

Sample Xcode project by Ziad Hamdieh

 

As an iOS engineer who has to write and maintain a lot of networking code, I often opt for using the Alamofire networking library (which, under the hood, uses URLSession) over using URLSession directly. This is unless I need to write a lightweight library with no external dependencies, which typically is not the case. In fact, at TextNow we decided to become early adopters of Alamofire 5. Because the public API for Alamofire 5 has changed significantly from the previous version, we thought an article on how to use Alamofire 5 might be of interest to fellow iOS developers.

When it comes to Alamofire 5, there are two fundamental uses. The first is basic: how to make HTTP calls against a REST API. The second, more advanced usage, might involve adding content to the header of your HTTP request for each and every outgoing call and do so in a way that does not have you repeat yourself. This article will show you how to use Alamofire 5 for both your basic and advanced needs. We even included an Alamofire Session extension that you can copy-and-paste to help you get off the ground quickly. So let’s get started!

The basics: two ways to send data

Consider this example: you need to tell your backend that the name of a particular mobile user is “Jonathan” and that Jonathan’s age is 65.

There are two obvious ways to send this data to your backend. The first way is as a URL-encoded query string. The second is in the body of the request in JSON format.

Query string vs. JSON:

As a query string, it might look like this:

{
  "Name": "Jonathan",
  "Age": 65
}

If our base URL is http://mybackend.com, our endpoint is /endpoint/<username> , and the user’s username is clarksdad , then the entire URL would be:

http://mybackend.com/myendpoint/users/clarksdad?name=jonathan&age=65

As JSON, this same data would look like this:

let url = "http://yourserver.com/users/\(username)"
session.POST(url, payload: userStruct)

The name and age of the user might differ each time, but that’s what a sample JSON payload looks like with the keys “Name” and “Age” staying constant. This payload would then get sent in the body of the request, leaving the URL to be simply:

http://mybackend.com/myendpoint/users/clarksdad

Modeling the data

Whether sending JSON or a query string, our data model must conform to Encodable. In Swift, dictionaries conform to Encodable, so you could use a dictionary. Our data model would then look as follows:

// data

let userDictionary = ["name": "Jonathan", "age": 65]

Or we could use a struct. Keys in dictionaries are strings and so dictionaries in this sense have weaker typing and so, all other things being equal, you might prefer structs. The struct would need to conform to either Codable or Encodable. An Encodable is all we really need because our goal is to send it, not receive it.

// data model
struct User: Encodable {
  let name: String
  let age: Int
}

Alamofire 5 accepts an Encodable and so, whether you are sending a query string or a JSON payload, both dictionaries and structs will work. Using a struct, a particular user would then look as follows:

let userStruct = User(name: "Jonathan", age: 65)

Sending the data

With Alamofire 5, we could send our user data by setting the parameters and encoder fields of the request method as follows. Sending a dictionary as URL encoded, for example, would look as follows:


// GET /users/<username>?name=Jonathan&age=65

let request = AF.request(
  "http://yourserver.com/users/\(username)", 
  method: .get,
  parameters: userStruct, // or `userDictionary` because both conform to `Encodable`
  encoder: URLEncodedFormParameterEncoder.default)

// Receive and decode the server's JSON response, 
// where `ExpectedResponse` is a struct that conforms to 
// either `Codable` or just `Decodable`.
request.responseDecodable(of: ExpectedResponse.self) { response in
  switch response.result {
  case let .success(result):
    // Do something useful with `result`, the decoded result of
    // type `ExpectedResponse` that you received from the server
  case let .failure(error):
    // Handle the error, a 404 for example.
  }
}

 

Sending the same user data as JSON looks very similar:

 


// POST /users/<username>, with user data sent as JSON body.
let request = AF.request(
   "http://yourserver.com/users/\(username)",
   method: .post,
   parameters: userStruct, // or `userDictionary` because both conform to `Encodable`
   encoder: JSONParameterEncoder.default)

// Receive and decode the server's JSON response.
request.responseDecodable(of: ExpectedResponse.self) { response in
   switch response.result {
   case let .success(result):
      // Do something useful with `result`, the decoded result of
      // type `ExpectedResponse` that you received from the server.
   
   case let .failure(error):
      // Handle the error, a 404 for example.
   }
}


As the code snippets above illustrate, the encoder parameter (in the request method) can be set to either

URLEncodedFormParameterEncoder.default

or…

JSONParameterEncoder.default

Depending on whether you need URL encoding or JSON format. The parameters parameter, admittedly a somewhat confusing name, is where our user data goes to be encoded and sent. The parameters field is very powerful because it can accept a dictionary or a Codable/Encodable struct. It can accept both because both are Encodables.

To recap, with Alamofire 5’s public API, sending a JSON body is not much different than sending a query string and both can be sent using a struct (or a dictionary).

Alamofire’s missing CRUD API

Wouldn’t it be nice if, instead of the above request calls, we could simply do the following:


let url = "http://yourserver.com/users/\(username)"
session.GET(url, queryItems: userDictionary)

let url = "http://yourserver.com/users/\(username)"
session.POST(url, payload: userStruct)

// GET url?queryString
let request = AF.GET(url, queryItems: userDictionary)

// Receive and decode the server's response.
request.responseDecodable(of: AddressResponse.self) { response in
    // Handle response from server.
    switch response.result {
    case let .success(addressResponse):
      // Do something useful with the decoded `AddressResponse`.
    case let .failure(error):
      // Handle the error, a 404 for example.
    }
}

That way, not only is there less to type, but we wouldn’t have to worry about accidentally ending up with a combination of function arguments that don’t make sense. In other words, wouldn’t it be nice if Alamofire had a CRUD API for common REST calls? Well, it could!

CRUD

The four basic operations required for persistent storage are create, read, update, and delete — CRUD for short. REST and SQL, for example, both support basic CRUD operations. Relational databases typically rely on SQL (“structured query language”) for interacting with and managing the data they store. Using SQL, we can create a new record (INSERT), read an existing record (SELECT), update a record (UPDATE), and delete a record (DELETE). In other words, SQL gives us the four basic CRUD operations. And so does REST.

A subset of the HTTP protocol, or more specifically a subset of HTTP methods, similarly gives us CRUD operations for REST and RESTful APIs. There is a create operation (POST), a read operation (GET), an update operation (PUT/PATCH), and a delete operation (DELETE). We could extend Alamofire’s Session class to have a CRUD API for REST by taking the two basic calls we cooked up in the previous section and treating them as building blocks. The following Alamofire extension, which you can copy-and-paste into your Xcode project and then massage to fit your backend, simplifies call sites by supporting all four basic CRUD operations along with PUT’s smaller cousin, PATCH. Here is the code you would need:


/// Alamofire's missing CRUD API for talking to RESTful backends.
extension Alamofire.Session {
  func GET(_ url: URLConvertible, query: [String: String] = [:]) -> DataRequest {
    return sendQueryParameters(
      url, 
      method: .get, 
      queryItems: query)
  }
  
  func POST<T: Encodable>(_ url: URLConvertible, payload: T) -> DataRequest {
    return sendPayload(url, method: .post, payload: payload)
  }
  
  func PUT<T: Encodable>(_ url: URLConvertible, payload: T) -> DataRequest {
    return sendPayload(url, method: .put, payload: payload)
  }
  
  func PATCH<T: Encodable>(_ url: URLConvertible, payload: T) -> DataRequest {
    return sendPayload(url, method: .patch, payload: payload)
  }
  
  func DELETE(_ url: URLConvertible, query: [String: String] = [:]) -> DataRequest {
    return sendQueryParameters(
      url, 
      method: .delete, 
      queryItems: query)
  }
}

// MARK: - Helpers

private extension Alamofire.Session {
  /// Sends the payload in the HTTP body as JSON.
  private func sendPayload<T: Encodable>(_ url: URLConvertible, method: HTTPMethod, payload: T) -> DataRequest {
    return request(
      url,
      method: method,
      parameters: payload,
      encoder: JSONParameterEncoder.default)
  }
  
  /// Sends the query parameters appended to the endpoint's path as URL encoded.
  private func sendQueryParameters(_ url: URLConvertible, method: HTTPMethod, queryItems: [String: String]) -> DataRequest {
    return request(
      url, 
      method: method, 
      parameters: queryItems,
      encoder: URLEncodedFormParameterEncoder.default)
  }
}

 

That simple extension gives Alamofire a more user-friendly API and covers most use cases. If your use case is slightly different, you can massage the code above to fit. (For example, the code above presupposes that all of your data to be sent as query strings is supplied using dictionaries while all of your data to be sent as JSON in the body of a request is supplied using structs.)

It is true, however, that the typical way to support CRUD endpoints in production using Alamofire today is to create an enum which acts as a collection of endpoints or HTTP methods to be exercised on an endpoint. You could even have one giant enum which houses all your endpoints in one place and so acts as a router. Either way, this enum would then have a computed method property of type HTTPMethod. For an example, see the Alamofire ‘advanced usage’ documentation.

Keeping this in mind, turning CRUD operations into functions — turning HTTP methods into Swift methods — might be seen as a bit of a throwback, perhaps brought about by personal nostalgia. Both are true: it is a throwback and it is not without nostalgia. However, I believe that in certain cases this can be appropriate. Firstly, it is intuitive to think of GETs and POSTs as methods. Secondly, unlike an enum-based router, it does not fall under advanced usage and does not require the level of comfort with Swift that the enum way requires— thinking of enums as similar to classes and structs, as something that can have computed properties and methods and, moreover, can switch on self to return the property value appropriate to the enum case. Thirdly, you might actually wish to limit the types of HTTP calls (e.g. in a large codebase maintained by a team rather than by you alone when the backend supports a well-defined limited set). For example, you might wish to eliminate the possibility of a GET with a body or a POST without one and perhaps completely disallow PATCH or PUT. Moreover, these three reasons aside, in a more advanced architecture, it is actually also possible to use the two approaches in tandem.

Whether this approach will scale, assuming you even need it to, might depend on whether you need to keep your options open or instead need to lock down a narrower set of HTTP calls.

Modifying the HTTP headers for all outgoing calls

Modifying an HTTP header in Alamofire is easy enough. For example:

request.headers.update(.authorization(bearerToken: token))

But if you need to append some content to HTTP headers of all outgoing requests, which is not uncommon, Alamofire can help with that, too. Typically, such content identifies the type of client (e.g. iOS vs. Android vs. web) and identifies the user (e.g. an authentication token), but could really be anything you need to add to the http header.

Essentially, whether by updating the headers of individual requests or by using an adapter, headers are a third way to send data in addition to the two ways we saw above (that is, in the request body or as a query string). After all, if an HTTP request can have a body, then it is not surprising that it can also have a head(er). Though I suppose when it comes to HTTP requests, when things get weird, it is more a case of the head of Professor Dowell than the Headless Horseman: all requests have headers, whereas not all have bodies (e.g. a GET is bodiless). But I digress.

A request adapter modifies the HTTP header for outgoing requests. The idea behind a request adapter is to write this piece of logic once, in a centralized location, so that we do not have to worry about it when making the kinds of network calls we were making above.

Imagine your backend requires that you send an “Authorization” HTTP header field with each and every request. HTTP header fields are key-value pairs (e.g. try it out in Postman) and so our key, conventionally, would then be “Authorization” (why the convention for authentication is called “authorization” — I am not sure). Moreover, your backend colleagues give you the following format requirement for the value of this key:

Authorization: Basic <token>

For example:

Authorization: Basic b3BlbiBzZXNhbWU=

(Try decoding the b3BlbiBzZXNhbWU= using a base64 decoder; and for a fun “do-along” article on security, see here.)

Because your backend is using this to authenticate users, the backend server will refuse to talk to you on any route if you don’t send this string. One elegant way to solve this problem is to intercept all outgoing call and have a request adapter “adapt” (that is, modify) each outgoing call by adding the “Authorization: Basic . . .” string to the HTTP header of each intercepted request.

For example, here is an implementation of a request adapter using Alamofire:


class MyRequestAdapter: Alamofire.RequestAdapter {
  func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (AFResult<URLRequest>) -> Void {
    var adaptedRequest = urlRequest // because `urlRequest` is a parameter and parameters are not mutable
    
    // INSERT MISSING CODE HERE: Read the `token` from somewhere, 
    // either from memory (e.g. a network manager singleton) or
    // from some secure location on disk (e.g. shared keychain).
    let token = <Your Token>
    adaptedRequest.headers.update(.authorization(bearerToken: token))      
    completion(.success(adaptedRequest))
  }
}

 

Then, unlike before, instantiate your Alamofire network session like so:

 


let adapter = MyRequestAdapter()
let interceptor = Interceptor(adapters: [adapter])

let session = Alamofire.Session(
  configuration: configuration, 
  interceptor: interceptor)

 

Voila! All your outgoing HTTP requests will now have the authorization header automatically appended, including the ones in the previous section, so long as they use the Alamofire session we instantiated with our request adapter above.

What did we learn?

Alamofire 5 is a powerful networking library that can help with all kinds of use cases: everything from basic CRUD operations to modifying HTTP headers, sending data in the body of a request to centralizing session management with a request adapter. In this article I showed how to make simple GET and POST calls using this latest version of Alamofire (section 1), how to extend Alamofire to have a CRUD API (the controversial section 2), and how to write a request adapter (section 3).

Happy coding!

Similar posts

No Comments

Leave a Reply

Your email address will not be published. Required fields are marked with *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.