Advertisement
Guest User

Untitled

a guest
Feb 28th, 2017
107
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.67 KB | None | 0 0
  1. # The Swift wrapper I wrote for our API
  2. Our application needed a new API wrapper. Here's how I made ours.
  3.  
  4. My goal with this wrapper was to find a concise, lightweight, functional and type-safe way to call all of our API endpoints cleanly while still being able to handle special cases when needed.
  5.  
  6.  
  7. ## Base
  8. First, we needed to support dev/prod URLs.
  9.  
  10. ```
  11. struct API {
  12. //MARK: URL
  13. static let prodUrl = "prod_url"
  14. static let devUrl = "dev_url"
  15.  
  16. static let isProd = true
  17.  
  18. static var baseUrl: String {
  19. return isProd ? prodUrl : devUrl
  20. }
  21. }
  22. ```
  23.  
  24. ## Routes
  25. Then, I needed to define the routes. To make it type safe, `enum` was the obvious choice. I replaced the actual things we use with some examples.
  26.  
  27. Swift allows enum cases to contain values (structs) with AND without argument labels, making it a nice way to pass the required parameters.
  28.  
  29. ```
  30. //MARK: Route defining
  31. extension API {
  32. struct Route {
  33. enum get {
  34. //-Store
  35. case products
  36. case product(String)
  37.  
  38. case purchases
  39. case purchase(String)
  40. }
  41.  
  42. enum post {
  43. //-Review
  44. case review(id: String, stars: String, text: String)
  45.  
  46. //-User
  47. case user(fullName: String, email: String, username: String, password: String)
  48. }
  49. }
  50. }
  51. ```
  52.  
  53. A problem with enum cases containing values is that they can't represent a raw value, which would have been nice to store the endpoints.
  54.  
  55. ## Endpoints
  56. Because of this issue, we need to make another function to get the actual url for that endpoint. This turned to be just fine, as some of our endpoints needed the ID to be a part of the path anyway.
  57.  
  58. ```
  59. //MARK: URL Paths
  60. extension API {
  61. static func endpoint(`for` g: API.Route.get) -> String {
  62. switch g {
  63. case .products: return "/products"
  64. case .product(let id): return "/products/" + id
  65.  
  66. case .purchases: return "/purchases"
  67. case .purchase(let id): return "/purchases/" + id
  68. }
  69. }
  70.  
  71. static func endpoint(`for` p: API.Route.post) -> String {
  72. switch p {
  73. case .review(let review): return "/rating/\(review.id)"
  74. case .purchase: return "/purchase"
  75. case .user: return "/user"
  76. }
  77. }
  78. }
  79. ```
  80.  
  81. ## Requests
  82. Now, to how we make an actual request. Obviously we use Alamofire for the heavy lifting here. I chose to use `get` as the function name instead of following Alamofire's `request(method:)` pattern, because it looks nicer IMO. Now, there is a couple of things going on here. First, we set request variable as a closure, because we do not want to generate the actual URL request until later. Then, we get the URL from the previous code block. Then, we switch on the URL. This is where this pattern starts to be awesome. We can special case where needed and use a default implementation if we don't need to do anything for this particular endpoint. The fictional "products" endpoint needs to have a default param that is always set, but all other endpoints can just be passed straight in. At last, we generate the request and set up response logging. More on that later.
  83.  
  84. ```
  85. extension API {
  86. //MARK: GET requests
  87. @discardableResult
  88. static func get(_ route: API.Route.get) -> DataRequest {
  89. var request: () -> DataRequest
  90. let url = baseUrl + endpoint(for: route)
  91.  
  92. switch route {
  93. case .products:
  94. let params = ["exclude_something":"true"]
  95. request = { Alamofire.request(url, method: .get, parameters: params) }
  96. default:
  97. request = { Alamofire.request(url, method: .get) }
  98. }
  99.  
  100. return request().responseJSON(completionHandler: { (response) in log(response)})
  101. }
  102.  
  103. }
  104. ```
  105.  
  106. ## Mirrors
  107. This is all fine and well, but Swift is more magic than that. Have you heard about a tiny little thing called "Mirror"? It allows you to iterate over an the properties of an object, ObjectiveC-style. You remember that enum cases can contain anonymous structs? See where this is going? That's right, we can magically use the contained value as params.
  108.  
  109. ```
  110. extension API {
  111. //MARK: POST requests
  112. @discardableResult
  113. static func post(_ route: API.Route.post) -> DataRequest {
  114. var request: () -> DataRequest
  115. let url = baseUrl + endpoint(for: route)
  116.  
  117. switch route {
  118. case let containedValue:
  119. guard let containedChild = Mirror(reflecting: containedValue).children.first?.value, let containedParams = Mirror(reflecting: containedChild).dictionary else { fallthrough }
  120. request = { Alamofire.request(url, method: .post, parameters: containedParams)}
  121. default:
  122. request = { Alamofire.request(url, method: .post, parameters: [:]) }
  123. }
  124.  
  125. return request().responseJSON(completionHandler: { (response) in log(response)})
  126. }
  127. }
  128. ```
  129.  
  130.  
  131. This uses a nifty little extension to convert a Mirror's children into a Dictionary.
  132.  
  133. ```
  134. extension Mirror {
  135. var dictionary: [String: Any]? {
  136. var paramDict: [String: Any] = [:]
  137. children.forEach({ (child) in
  138. if let key = child.label, let value = (child.value as AnyObject?) {
  139. paramDict[key] = value
  140. }
  141. })
  142. return paramDict.isEmpty ? nil : paramDict
  143. }
  144. }
  145. ```
  146.  
  147. ## Logging
  148. Then, I wanted to log something for all network requests. As you can see, all these get/post request functions calls `request().responseJSON(completionHandler: { (response) in log(response)})` when returning.
  149.  
  150. ```
  151. extension API {
  152. //MARK: Logging
  153. //What we log for EVERY request.
  154. fileprivate static let logLevels: [LogLevel] = [.success, .requestedUrl, .statusCode]
  155. fileprivate enum LogLevel { case none, success, requestedUrl, statusCode, response }
  156.  
  157. static func log(_ request: DataResponse<Any>) {
  158. if logLevels.contains(.none) { return }
  159.  
  160. let logString = logLevels.flatMap { (level) -> String? in
  161. switch level {
  162. case .none: return nil
  163. case .requestedUrl: return request.request?.url?.absoluteString
  164. case .statusCode: return "\(request.response?.statusCode ?? -9001)"
  165. case .success: return "\(request.result)"
  166. case .response: return request.result.value != nil ? "\(request.result.value!)" : nil
  167. }
  168. }
  169.  
  170. print("API: ", logString)
  171. }
  172. }
  173. ```
  174.  
  175. This gives us a neat little log every time something gets called, and lets us customise what we log very easily.
  176.  
  177. # Serialisation
  178. Kind of off topic, but I also wanted to be able to parse the JSON response into objects. Alamofire allows us to extend the response structs, so here's how I did that.
  179.  
  180. ```
  181. //MARK: Response Object Serialization
  182. protocol JsonInitable {
  183. init(_ jsonDict: [String: AnyObject])
  184. }
  185.  
  186. extension DataRequest {
  187. @discardableResult
  188. func responseObjects<T: JsonInitable>(valueKey: String = "results", pageKey:String? = nil, completion:@escaping (_ objects: [T], _ pageValue:String?, _ error: Error?) -> Void) -> Self {
  189. responseJSON { (response) in
  190. if let responseDict = response.result.value as? [String: AnyObject], let objectJsons = responseDict[valueKey] as? [[String: AnyObject]] {
  191. let objects = objectJsons.flatMap(T.init)
  192. let pageValue = (pageKey != nil) ? responseDict[pageKey!] as? String : nil
  193. completion(objects, pageValue, nil)
  194. }
  195. }
  196.  
  197. return self
  198. }
  199.  
  200. @discardableResult
  201. func responseObject<T: JsonInitable>(valueKey: String = "result", completion:@escaping (_ object: T, _ error: Error?) -> Void) -> Self {
  202. responseJSON { (response) in
  203. if let responseDict = response.result.value as? [String: AnyObject], let objectJson = responseDict[valueKey] as? [String: AnyObject] {
  204. completion(T(objectJson), nil)
  205. }
  206. }
  207.  
  208. return self
  209. }
  210. }
  211. ```
  212.  
  213.  
  214. ## Usage
  215. Now, lets see how it looks in action!
  216.  
  217. ```
  218. API.post(.user(fullName: "Name Lastname", email: "name@domain.tld", username: "username", password: "Password123")) //If we don't care about the return value.
  219.  
  220. API.get(.products).responseObjects { (products: [Product], _, error) in
  221. print(products)
  222. }
  223.  
  224. ```
  225.  
  226. Pretty neat, eh? :D
  227.  
  228. ## Conclusion
  229. Now we have an API wrapper that is type safe, allows us to add new endpoints quickly - unless we need to do something special we just need to add an enum case and the corresponding path in the `endpoint(for:)`. Swift forces `switch` statements to be exhaustive, so you can't fail to forget that. Also, Xcode actually manages to autocomplete this every time after you type API.%method%(.#HERE#), which is nice.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement