Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper usage of the Alamofire's URLRequestConvertible

I've read couple tutorials, README from @mattt but can't figure out couple things.

  1. What is the proper usage of URLRequestConvertible in real world API? It looks like if I will create one router by implementing URLRequestConvertible protocol for all API - it will be barely readable. Should I create one Router per endpoint?

  2. Second question most likely caused by lack of experience with Swift language. I can't figure out why enum is used for building router? Why we don't use class with static methods? here is an example (from Alamofire's README)

    enum Router: URLRequestConvertible {     static let baseURLString = "http://example.com"     static let perPage = 50      case Search(query: String, page: Int)      // MARK: URLRequestConvertible      var URLRequest: NSURLRequest {         let (path: String, parameters: [String: AnyObject]?) = {             switch self {             case .Search(let query, let page) where page > 1:                 return ("/search", ["q": query, "offset": Router.perPage * page])             case .Search(let query, _):                 return ("/search", ["q": query])             }         }()          let URL = NSURL(string: Router.baseURLString)!         let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(path))         let encoding = Alamofire.ParameterEncoding.URL          return encoding.encode(URLRequest, parameters: parameters).0     } } 
  3. There are 2 ways to pass parameters:

    case CreateUser([String: AnyObject]) case ReadUser(String) case UpdateUser(String, [String: AnyObject]) case DestroyUser(String) 

    and (say user has 4 parameters)

    case CreateUser(String, String, String, String) case ReadUser(String) case UpdateUser(String, String, String, String, String) case DestroyUser(String) 

    @mattt is using the first one in the example. But that will lead to "hardcoding" parameters' names outside the router (e.g. in UIViewControllers). Typo in parameter name can lead to error.
    Other people are using 2nd option, but in that case it not obvious at all what each parameter represents.
    What will be the right way to do it?

like image 612
OgreSwamp Avatar asked Feb 04 '15 23:02

OgreSwamp


People also ask

What is URLRequestConvertible?

The URLRequestConvertible protocol is a lightweight way to ensure a given object can create a valid NSURLRequest . There's not really a strict set of rules or guidelines that exist forcing you to use this protocol in any particular way.

Why use alamofire?

Alamofire makes developing networking layers easier, faster and much cleaner. Another great benefit of using it is that it can be studied and its code is available. This can help programmers because it's a well-written framework.

What is URLConvertible?

public protocol URLConvertible. Types adopting the URLConvertible protocol can be used to construct URL s, which can then be used to construct URLRequests . asURL() Returns a URL from the conforming instance or throws.


2 Answers

Great questions. Let's break down each one individually.

What is the proper usage of URLRequestConvertible in real world API?

The URLRequestConvertible protocol is a lightweight way to ensure a given object can create a valid NSURLRequest. There's not really a strict set of rules or guidelines that exist forcing you to use this protocol in any particular way. It's merely a convenience protocol to allow other objects to store state required to properly create the NSURLRequest. Some more information relating to Alamofire can be found here.

Should I create one Router per endpoint?

Definitely not. That would defeat the entire purpose of using an Enum. Swift Enum objects are amazingly powerful allowing you to share a large amount of common state, and switch on the parts that actually different. Being able to create an NSURLRequest with something as simple as the following is really powerful!

let URLRequest: NSURLRequest = Router.ReadUser("cnoon") 

I can't figure out why enum is used for building router? Why we don't use class with static methods?

An enum is being used because it is a much more concise way of expressing multiple related objects under a common interface. All the methods are shared between all the cases. If you used static methods, you'd have to have a static method for each case for each method. Or you would have to use an Obj-C styled enum inside the object. Here's a quick example of what I mean.

enum Router: URLRequestConvertible {     static let baseURLString = "http://example.com"      case CreateUser([String: AnyObject])     case ReadUser(String)     case UpdateUser(String, [String: AnyObject])     case DestroyUser(String)      var method: Alamofire.HTTPMethod {         switch self {         case .CreateUser:             return .post         case .ReadUser:             return .get         case .UpdateUser:             return .put         case .DestroyUser:             return .delete         }     }      var path: String {         switch self {         case .CreateUser:             return "/users"         case .ReadUser(let username):             return "/users/\(username)"         case .UpdateUser(let username, _):             return "/users/\(username)"         case .DestroyUser(let username):             return "/users/\(username)"         }     } } 

To get the method of any of the different endpoints, you can call the same method without having to pass in any parameters to define what type of endpoint you are looking for, it's already handled by the case you select.

let createUserMethod = Router.CreateUser.method let updateUserMethod = Router.UpdateUser.method 

Or if you want to get the path, same types of calls.

let updateUserPath = Router.UpdateUser.path let destroyUserPath = Router.DestroyUser.path 

Now let's try the same approach using static methods.

struct Router: URLRequestConvertible {     static let baseURLString = "http://example.com"      static var method: Method {         // how do I pick which endpoint?     }      static func methodForEndpoint(endpoint: String) -> Method {         // but then I have to pass in the endpoint each time         // what if I use the wrong key?         // possible solution...use an Obj-C style enum without functions?         // best solution, merge both concepts and bingo, Swift enums emerge     }      static var path: String {         // bummer...I have the same problem in this method too.     }      static func pathForEndpoint(endpoint: String) -> String {         // I guess I could pass the endpoint key again?     }      static var pathForCreateUser: String {         // I've got it, let's just create individual properties for each type         return "/create/user/path"     }      static var pathForUpdateUser: String {         // this is going to get really repetitive for each case for each method         return "/update/user/path"     }      // This approach gets sloppy pretty quickly } 

NOTE: If you don't have many properties or functions that switch on the cases, then an enum doesn't present many advantages over a struct. It is simply an alternative approach with different syntactic sugar.

Enums can maximize state and code reuse. The associated values also allow you to do some really powerful things like grouping objects that are somewhat similar, but have incredibly different requirements...such as NSURLRequest creation.

What is the right way to construct parameters for enum cases to improve readability? (had to mash this one together)

That's a terrific question. You've already laid out two possible options. Let me add a third that may suit your needs a bit better.

case CreateUser(username: String, firstName: String, lastName: String, email: String) case ReadUser(username: String) case UpdateUser(username: String, firstName: String, lastName: String, email: String) case DestroyUser(username: String) 

In cases where you have associated values, I think it can be helpful to add explicit names for all the values in the tuple. This really helps build the context. The downside is that you then have to redeclare those values in your switch statements like so.

static var method: String {     switch self {     case let CreateUser(username: username, firstName: firstName, lastName: lastName, email: email):         return "POST"     default:         return "GET"     } } 

While this gives you a nice, consistent context, it gets pretty verbose. Those are your three options at the moment in Swift, which one is the correct one to use depends on your use case.


Update

With the release of 🔥🔥 Alamofire 4.0 🔥🔥, the URLRequestConvertible can now be MUCH smarter and can also throw. We've added full support into Alamofire for handling invalid requests and generating sensible errors through the response handlers. This new system is documented in detail in our README.

like image 117
cnoon Avatar answered Sep 20 '22 21:09

cnoon


Here is the up to date enum Router in Swift 3, which is recommend on Alamofire's Github. I hope you find it useful in terms of how to properly implement a Router with URLRequestConvertible.

import Alamofire  enum Router: URLRequestConvertible {     case createUser(parameters: Parameters)     case readUser(username: String)     case updateUser(username: String, parameters: Parameters)     case destroyUser(username: String)      static let baseURLString = "https://example.com"      var method: HTTPMethod     {         switch self {         case .createUser:             return .post         case .readUser:             return .get         case .updateUser:             return .put         case .destroyUser:             return .delete         }      }      var path: String     {         switch self {         case .createUser:             return "/users"         case .readUser(let username):             return "/users/\(username)"         case .updateUser(let username, _):             return "/users/\(username)"         case .destroyUser(let username):             return "/users/\(username)"         }     }      // MARK: URLRequestConvertible      func asURLRequest() throws -> URLRequest     {         let url = try Router.baseURLString.asURL()          var urlRequest = URLRequest(url: url.appendingPathComponent(path))         urlRequest.httpMethod = method.rawValue          switch self {         case .createUser(let parameters):             urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)         case .updateUser(_, let parameters):             urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)         default:             break         }          return urlRequest     } } 
like image 29
arauter Avatar answered Sep 23 '22 21:09

arauter