REST Client in Swift with Promises
Diving deeper into Swift, I am examining different ways of improving my architecture, and making the best decisions when creating new applications. For me, architecture and expressiveness in the code is much more important that a complicated algorithm that boosts performance over 100%.
One thing I am called to do very often, is set up an HTTP client that will handle network operations, access tokens, and perform bandwidth throttling. So, I would like to share with you my idea of approaching the creation of an HTTP client with Swift.
We will use the following frameworks:
- Alamofire for HTTP Calls.
- ObjectMapper for mapping HTTP Responses to objects
- PromiseKit for making your code more… promising! I am a very strong applicant of promises, both in Javascript and iOS, and I strongly recommend it for making asynchronous code clearer and more scalable, without compromising flexibility.
Lets suppose you have this kind of API:
{ "error" : null, "response" : "…", "success" : true }
Let’s suppose that all API responses have this format where:
- Error holds an explanation of the error that happened in the server (missing arguments, invalid tokens, etc). Let’s imagine that error is an object holds two values: “errorMessage” and “errorCode”, both strings that are returned from the server.
- Response holds an object indicating the response depending on the request (“for example, the response can be an array of objects, indicating a product list, or the user’s profile details)
- Success is a boolean indicating that the operation went well. If success is “false” the “error” will hold an explanation of what went wrong.
For this example, I opted to use the root object containing ‘success’, ‘error’ and ‘response’, since I usually like to distinguish between logical errors, and server errors. For example, trying to login with invalid credentials is a logical error, and would return ‘false’ in the ‘success’ field, with an error object in the ‘error’ field. And an invalid kind of argument given, or any server error, would be an error 500, or something similar. Everything else, is a success, accompanied by a ‘response’ object.
Simple HTTP client implementation (code)
We will use a simple Swift Enum as the router for our application. By having an enum, we can have autocompletion for our server routes, and avoid redundant duplicate strings when trying to access a server resource / path.
public enum HTTPRouter { private static let baseURLString = "http://example.com/mobileapi" case Articles case CheckSessionStatus public var URLString : String { let path : String = { switch self { case .Articles: return "feed/Articles" } }() return HTTPRouter.baseURLString + path; } }
Before we start implementing our HTTP Client, we should make the most basic parseable object. Here’s what we know so far, that will play a role into our model design:
- The base response is the same. There is always an “error”, “response” and “success” property in the response.
- Those base properties are only used to determine if there is an error during the request to the server. If the request is successful, we don’t need them anymore.
- If the request is successful, what we care about, is the object contained inside the “response” object.
- If the request fails, we show the error, and we don’t call the “success” callback, in case we had any.
Based on these assumptions, let’s begin modelling our response objects using ObjectMapper’s functionality.
We model the basic response as a generic, since the “response” object can be any type of object. Node that the type of T is defined to be a Mappable object. This is by design. It is our responsibility to provide mappable objects to the “response” objects in order for it to be parsed.
class APIResponse<T : Mappable>: Mappable { var success: Bool = false var response: T? var error : APIError? required init?(_ map: Map) { } func mapping(map: Map) { success <- map["success"] response <- map["response"] error <- map["error"] } } class APIError : Mappable { var errorName : String? var errorCode : String? required init?(_ map: Map) { } func mapping(map : Map){ errorName <- map["errorMessage"] errorCode <- map["errorCode"] } }
We also define an error class. Although I tend to refrain myself from subclassing NSError, in this case, I opted to do it, in order to help me explain better this use case.
class APIErrorResult : NSError { var apiError : APIError? init(errorFromAPI : APIError?){ let userInfo : [String : AnyObject] = { if let err = errorFromAPI{ return ["apiError" : err] }else{ return ["apiError" : "Generic Error"] } }() super.init(domain: "com.oramind.error", code: -101, userInfo: userInfo) self.apiError = errorFromAPI } func errorMessage() -> String { guard let apierr = self.apiError, let errName = apierr.errorName else { return "Generic error" } return errName; } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } }
Based on these models, we proceed with the implementation of the HTTP Client
class HTTPClient : NSObject { static let sharedInstance = HTTPClient() //MARK: Helper functions func post<T : Mappable>(route : HTTPRouter, parameters : [String : AnyObject]? = nil) -> Promise<T?> { return self.httpOperation(.POST, route: route, parameters: parameters) } func get<T : Mappable>(route : HTTPRouter, parameters : [String : AnyObject]? = nil) -> Promise<T?> { return self.httpOperation(.GET, route: route, parameters : parameters); } private func httpOperation<T : Mappable>(method : Alamofire.Method, route : HTTPRouter, var parameters : [String : AnyObject]? = nil) -> Promise<T?> { return Promise<T?> { (fulfill, reject) -> Void in func parsingError(erroString : String) -> NSError { return NSError(domain: "com.oramind.error", code: -100, userInfo: nil) } request(method, route.URLString, parameters: parameters).responseJSON { (response) -> Void in if let data = response.data { print("\(String(data: data, encoding: NSUTF8StringEncoding))"); } print(response.result.value) // result of response serialization if let error = response.result.error { reject(error) //network error }else { if let apiResponse = Mapper<APIResponse<T>>().map(response.result.value) { if apiResponse.success { fulfill(apiResponse.response) }else{ if let logicalerror = apiResponse.error { reject(APIErrorResult(errorFromAPI: logicalerror)) }else{ reject(APIErrorResult(errorFromAPI: nil)) } } }else{ let err = NSError(domain: "com.oramind.error", code: -101, userInfo: nil) reject(err) } } } } } }
How it works
The general concept is that when downloading the data, it is parsed and made into an object, which is then returned inside a Promise. The “magic” happens when determining the class and kind of parsing for the object to be returned.
This is done through specialising the T parameter when calling HTTPClient’s ‘get()’ or ‘post()’. By specializing it, the mapper knows which mapping class to call when downloading the data.
Consider the following JSON:
{ "success" : true, "error" : null, "response" : { "total" : 102314, "articles" : [ { "title" : "Article title 1", "author" : "Billy bob", "date" : 115948273 }, { "title" : "Article title 2", "author" : "Benny bee", "date" : 1152948273 }, { "title" : "Article title 3", "author" : "Jenny Kais", "date" : 115948273 }, { "title" : "Article title 4", "author" : "Harboreus Me", "date" : 115948273 } ] } }
If we want to map it, we can map it like this, with ObjectMapper;
class APIArticle : Mappable { var title : String? var author : String? var date : NSDate? required init?(_ map: Map) { } func mapping(map: Map) { title <- map["title"] author <- map["author"] date <- map["date"] } } class APIResponseArticles : Mappable { var total : Int = 0 var articles = [APIArticle]() required init?(_ map: Map) { } func mapping(map: Map) { articles <- map["articles"] total <- map["total"] } }
We need to only map the “response” content. Remember that the outer element, the one which contains “success” and “error” is already parsed. It will be used to determine if an error has happened when the JSON arrives, but after that step, we only need the content of the ‘response’ object to be returned to us.
We can now use our HTTPClient, and parse our first response.
//make a request to get the articles, then specify in the 'then' block that you want the server response //to be parsed as 'APIResponseArticles' //You need to specify it, otherwise the compiler will not know exactly how to parse the response, and will //not compile the code. //The 'response' is always optional. Some APIs can return 'null' as a valid response, making you to rely on the //value of the status (200), or, in this case, the server may respond with success = true, but with no content //in the 'response' object of the JSON. HTTPClient.sharedInstance.get(HTTPRouter.Articles).then { (response : APIResponseArticles?) -> Void in //this code will be executed only if parsing is completed and there is no networking or logical error //from the server if let actualResponse = response { //do something with the response here } } //make a request, and parse a generic response, whose content is of no interest to you HTTPClient.sharedInstance.get(HTTPRouter.Articles).then { (response : APIResponseGeneric?) -> Void in if let genericResponse = response { //do something with the response here } }
Conclusion
You can now chain HTTP requests, automatically parse the responses, and have a nice and clean HTTP client as the basis of all your requests. I suppose that this methodology can work for SOAP Requests, too, with a few modifications.
Depending on the structure of your API, you will probably have to change your approach with parsing.