Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # The Swift wrapper I wrote for our API
- Our application needed a new API wrapper. Here's how I made ours.
- 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.
- ## Base
- First, we needed to support dev/prod URLs.
- ```
- struct API {
- //MARK: URL
- static let prodUrl = "prod_url"
- static let devUrl = "dev_url"
- static let isProd = true
- static var baseUrl: String {
- return isProd ? prodUrl : devUrl
- }
- }
- ```
- ## Routes
- 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.
- Swift allows enum cases to contain values (structs) with AND without argument labels, making it a nice way to pass the required parameters.
- ```
- //MARK: Route defining
- extension API {
- struct Route {
- enum get {
- //-Store
- case products
- case product(String)
- case purchases
- case purchase(String)
- }
- enum post {
- //-Review
- case review(id: String, stars: String, text: String)
- //-User
- case user(fullName: String, email: String, username: String, password: String)
- }
- }
- }
- ```
- 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.
- ## Endpoints
- 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.
- ```
- //MARK: URL Paths
- extension API {
- static func endpoint(`for` g: API.Route.get) -> String {
- switch g {
- case .products: return "/products"
- case .product(let id): return "/products/" + id
- case .purchases: return "/purchases"
- case .purchase(let id): return "/purchases/" + id
- }
- }
- static func endpoint(`for` p: API.Route.post) -> String {
- switch p {
- case .review(let review): return "/rating/\(review.id)"
- case .purchase: return "/purchase"
- case .user: return "/user"
- }
- }
- }
- ```
- ## Requests
- 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.
- ```
- extension API {
- //MARK: GET requests
- @discardableResult
- static func get(_ route: API.Route.get) -> DataRequest {
- var request: () -> DataRequest
- let url = baseUrl + endpoint(for: route)
- switch route {
- case .products:
- let params = ["exclude_something":"true"]
- request = { Alamofire.request(url, method: .get, parameters: params) }
- default:
- request = { Alamofire.request(url, method: .get) }
- }
- return request().responseJSON(completionHandler: { (response) in log(response)})
- }
- }
- ```
- ## Mirrors
- 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.
- ```
- extension API {
- //MARK: POST requests
- @discardableResult
- static func post(_ route: API.Route.post) -> DataRequest {
- var request: () -> DataRequest
- let url = baseUrl + endpoint(for: route)
- switch route {
- case let containedValue:
- guard let containedChild = Mirror(reflecting: containedValue).children.first?.value, let containedParams = Mirror(reflecting: containedChild).dictionary else { fallthrough }
- request = { Alamofire.request(url, method: .post, parameters: containedParams)}
- default:
- request = { Alamofire.request(url, method: .post, parameters: [:]) }
- }
- return request().responseJSON(completionHandler: { (response) in log(response)})
- }
- }
- ```
- This uses a nifty little extension to convert a Mirror's children into a Dictionary.
- ```
- extension Mirror {
- var dictionary: [String: Any]? {
- var paramDict: [String: Any] = [:]
- children.forEach({ (child) in
- if let key = child.label, let value = (child.value as AnyObject?) {
- paramDict[key] = value
- }
- })
- return paramDict.isEmpty ? nil : paramDict
- }
- }
- ```
- ## Logging
- 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.
- ```
- extension API {
- //MARK: Logging
- //What we log for EVERY request.
- fileprivate static let logLevels: [LogLevel] = [.success, .requestedUrl, .statusCode]
- fileprivate enum LogLevel { case none, success, requestedUrl, statusCode, response }
- static func log(_ request: DataResponse<Any>) {
- if logLevels.contains(.none) { return }
- let logString = logLevels.flatMap { (level) -> String? in
- switch level {
- case .none: return nil
- case .requestedUrl: return request.request?.url?.absoluteString
- case .statusCode: return "\(request.response?.statusCode ?? -9001)"
- case .success: return "\(request.result)"
- case .response: return request.result.value != nil ? "\(request.result.value!)" : nil
- }
- }
- print("API: ", logString)
- }
- }
- ```
- This gives us a neat little log every time something gets called, and lets us customise what we log very easily.
- # Serialisation
- 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.
- ```
- //MARK: Response Object Serialization
- protocol JsonInitable {
- init(_ jsonDict: [String: AnyObject])
- }
- extension DataRequest {
- @discardableResult
- func responseObjects<T: JsonInitable>(valueKey: String = "results", pageKey:String? = nil, completion:@escaping (_ objects: [T], _ pageValue:String?, _ error: Error?) -> Void) -> Self {
- responseJSON { (response) in
- if let responseDict = response.result.value as? [String: AnyObject], let objectJsons = responseDict[valueKey] as? [[String: AnyObject]] {
- let objects = objectJsons.flatMap(T.init)
- let pageValue = (pageKey != nil) ? responseDict[pageKey!] as? String : nil
- completion(objects, pageValue, nil)
- }
- }
- return self
- }
- @discardableResult
- func responseObject<T: JsonInitable>(valueKey: String = "result", completion:@escaping (_ object: T, _ error: Error?) -> Void) -> Self {
- responseJSON { (response) in
- if let responseDict = response.result.value as? [String: AnyObject], let objectJson = responseDict[valueKey] as? [String: AnyObject] {
- completion(T(objectJson), nil)
- }
- }
- return self
- }
- }
- ```
- ## Usage
- Now, lets see how it looks in action!
- ```
- API.post(.user(fullName: "Name Lastname", email: "name@domain.tld", username: "username", password: "Password123")) //If we don't care about the return value.
- API.get(.products).responseObjects { (products: [Product], _, error) in
- print(products)
- }
- ```
- Pretty neat, eh? :D
- ## Conclusion
- 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