As I was playing with my GitHub project about networking I thought it would be interesting to try some different ways to implement the RESTClient protocol. In this article I’m going to show how I used Alamofire and then Moya (which itself uses Alamofire) instead of my custom network layer based on URLSession.
I’m going to use code from this GitHub project https://github.com/gualtierofrigerio/NetworkingExample
and I’ve written a couple of articles already about this example, one about Promises https://www.gfrigerio.com/callbacks-vs-promises/ and another about RxSwift https://www.gfrigerio.com/getting-started-with-rxswift/
To get you started really quickly the example uses a public API to retrieve users, albums and pictures. The service returns JSONs and doesn’t require authentication, so very simple setup required. You often, hopefully, have to authenticate in real life scenario, and frameworks like Alamofire and Moya can help.
The project started to demonstrate the use of Promises vs Callbacks, so I have a protocol to support both, but the implementation, in the networking layer, is pretty similar between the two.
This is the standard, or vanilla if you prefer, networking implementation of my RESTClient protocol https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/RESTClient.swift
protocol RESTClient {
func getData(atURL url:URL, completion: @escaping (Data?) -> Void)
func getData(atURL url:URL) -> Promise<Data>
}
func getData(atURL url: URL, completion: @escaping (Data?) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url) { (data, response, error) in
completion(data)
}
task.resume()
}
func getData(atURL url: URL) -> Promise<Data> {
let promise = Promise<Data>()
let session = URLSession.shared
let task = session.dataTask(with: url) { (data, response, error) in
if let err = error {
promise.reject(error: err)
}
else {
if let data = data {
promise.resolve(value: data)
}
else {
let unknowError = NSError(domain: "", code : 0, userInfo: nil)
promise.reject(error: unknowError)
}
}
}
task.resume()
return promise
}
So two functions to get data, one with a callback with an optional Data parameter and another one returning a Promise.
As you can see the latter looks a little more complex, but only because I need to reject or resolve the promise after checking if I have an error, if data is not nil. In this case using URLSession is pretty straightforward, as I only need to provide an URL and I need no HTTP headers, no authentication and the method is GET. It is of course possible to configure everything with the URLSession. Instead of passing a simple URL you can call dataTask with a URLRequest, that allows you to set more stuff like HTTP headers and the method. Whenever I’ll have a more complex example with some public REST APIs requiring authentication or some custom header to set I’ll be happy to add them to the example. For now let’s keep it simple.
Alamofire
Alamofire, or AFNetworking for our Objective-C friends, is a networking library that provides an interface on top of the Apple networking layer, in iOS and macOS. It is really popular, I’ve used it myself for some consulting projects as many companies prefer it over a custom implementation.
One of the things I like about Alamofire is function chaining that I’ll show you in a minute, and I’d like to write a separate article about.
First let me provide you the link of the GitHub project https://github.com/Alamofire/Alamofire
and you can install it via CocoaPods or Carthage. If you download my GitHub example remember to run pod install as I didn’t commit the entire Pods directory.
This is my RESTClient implementation with Alamofire https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/RESTClientAF.swift
func getData(atURL url: URL, completion: @escaping (Data?) -> Void) {
Alamofire.request(url)
.responseData { responseData in
completion(responseData.data)
}
}
func getData(atURL url: URL) -> Promise<Data> {
let promise = Promise<Data>()
Alamofire.request(url)
.responseData { responseData in
if let data = responseData.data {
promise.resolve(value: data)
}
else {
promise.reject(error: responseData.error ?? NSError())
}
}
return promise
}
The first function is just one line of code, very simple. I told you about function chaining, let me show it to you with another piece of code implementing the same getData function
Alamofire.request(url)
.responseData { responseData in
completion(responseData.data)
}
.responseString { responseString in
guard responseString.result.isSuccess else {
print("error \(responseString.result.error!)")
return
}
print("resposeString = \(responseString.value)")
}
.responseJSON { responseJSON in
guard responseJSON.result.isSuccess,
let dictionary = responseJSON.result.value as? [String:Any] else {
print("error \(responseJSON.result.error)")
return
}
print("responseJSON dictionary = \(dictionary)")
}
As you can see from the example above you can chain various responses together, so you can get called with Data, with a String and with a JSON. I really like this technique and I plan to write a separate article about it.
What if you need authentication? Another chained function to call:
Alamofire.request(url, method:.get))
.responseData { responseData in
completion(responseData.data)
}
.authenticate(user: "username", password: "password")
Alamofire.request(url)
.responseData { responseData in
completion(responseData.data)
}.validate(statusCode: [200, 201])
In the example I specified the HTTP method in the request call. You can even set parameters. Another function you can chain is validate, in the example I want to consider valid only HTTP status codes 200 and 201, otherwise I’ll get a failure.
As you can see once you start adding parameters, or authentication, or validation Alamofire seems like a good choice, you keep it simple as you can provide additional configuration by making function calls after the first one.
I like it, but let’s see how we can have another layer of abstraction.
Moya
As I said Moya is another abstraction on top of Alamofire, so is not taking care of the communication itself but provides a different approach. This is the GitHub page of the project https://github.com/Moya/Moya
and Alamofire as you can imagine is a dependency, so if you add Moya to your Podfile you’ll get Alamore as well, but you’re not going to use it directly.
Before showing you the code let me introduce 3 concepts: the Provider the Target and the Endpoint.
The Provider is the object we’ll use to make network requests, initialising it with a Target. What’s that? A Target describes a set of API, in our example the remote REST service we call to get users and pictures, each of them reachable at a particular Endpoint.
So how do we make a request? You can find the implementation here https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/RESTClientMoya.swift
First we need to describe the Target, and we can do it by using our Entity enum and extend TargetType.
extension Entity : TargetType {
var baseURL: URL {
return URL(string: "https://jsonplaceholder.typicode.com")!
}
var path: String {
switch self {
case .Album:
return "/albums"
case .Picture:
return "/photos"
case .User:
return "/users"
}
}
var method: Moya.Method {
return .get
}
var sampleData: Data {
return Data()
}
public var task: Task {
return .requestParameters(parameters: [:], encoding: URLEncoding.default)
}
var headers: [String : String]? {
return ["Content-Type" : "application/json"]
}
}
Moya will use this computed variables as the configuration for the network request. You can provide the baseURL, then the path for each of the Endpoints configured in your enum. In my previous example I used a similar technique to get the URL of each entity, but Moya takes in even further by letting you specify the HTTP method and the HTTP headers. You then have a Task, another enum describing as the name suggests the task you want to use. It can be a simple request, a request with parameters (like in the example), it can be an upload instead of a download and can use Multipart form data. You can even set a mock data with sampleData, useful for testing.
We’re using an enum, so for each of the variables we can have the switch self statement. In our example every request is a HTTP GET, but imagine we could upload a picture, we’d have another Endpoint with a different Method and a different Task, and different headers maybe.
And here’s how to make the request
let provider = MoyaProvider<Entity>()
func getData(forEntity entity:Entity, completion: @escaping (Data?) -> Void) {
provider.request(entity) { result in
switch result {
case let .success(response):
completion(response.data)
case .failure:
completion(nil)
}
}
}
func getData(forEntity entity: Entity) -> Promise<Data> {
let promise = Promise<Data>()
provider.request(entity) { result in
switch result {
case let .success(response):
promise.resolve(value: response.data)
case let.failure(error):
promise.reject(error: error)
}
}
return promise
}
First we create a MayaProvider object, with Entity as the type. To make a request for a particular Endpoint we call provider.request and as you can see we have another enum as a result, so we can switch between success, with the associated value response, or failure with the error.
At the beginning of this section I said Moya provides a different approach, and you can see it if you compare this functions with the one I used with Alamofire. My RESTClient protocol had two functions to get data from a URL, it didn’t specify a particular endpoint. For the sake of compiling the project and use the class inside of my sample project I actually implemented an extension that takes the URL and figures out which entity you want to get so it can call getData(forEntity:). I did that only to reuse all the existing code, as I referenced the project in a few articles already.
I’ve used protocols and was able to change the implementation of the RESTClient with just a line of code, but another layer between the data source and the actual network layer would have allowed me to use Moya or Alamofire or my custom implementation.
What I like about Moya is being able to have all the endpoints of a particular service in one enum and being able to configure everything there, with convenient switch statements. Using only Alamofire is a good solution as well, but in this case you have to provide some of the functionalities on top of that.
There isn’t a right or wrong approach, or the one and only framework every good developer should use. I think it is important to know what the alternatives are, and maybe you don’t need to import Moya and Alamofire but at least reading about them could suggest you which path to follow, or which techniques (like using the enum or chaining functions) you can adopt in your own code.
That’s all for now, but I plan to edit the article in the future when I’ll implement something more like uploads or file downloads with a progress bar, so you’ll see some more of Alamofire and Moya.