Proper usage of the Alamofire's URLRequestConvertible
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.
Singleton pattern and proper use of Alamofire's URLRequestConvertible
These are some really good questions. Let me attempt to answer each one in turn.
Do I create an enum router which implements URLRequestConvertible for each model in my model layer?
This is a great question and unfortunately there's no one perfect answer. There are certainly some ways that you could extend the Router
pattern to accommodate multiple object types. The first option would be to add more cases to support another object type. However, this gets hairy pretty quickly when you get more than 6 or 7 cases. Your switch statements just start to get out-of-control. Therefore, I wouldn't recommend this approach.
Another way to approach the problem is by introducing generics into the Router
.
RouterObject Protocol
protocol RouterObject {
func createObjectPath() -> String
func readObjectPath(identifier: String) -> String
func updateObjectPath(identifier: String) -> String
func destroyObjectPath(identifier: String) -> String
}
Model Objects
struct User: RouterObject {
let rootPath = "/users"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
struct Company: RouterObject {
let rootPath = "/companies"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
struct Location: RouterObject {
let rootPath = "/locations"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
Router
let baseURLString = "http://example.com"
var OAuthToken: String?
enum Router<T where T: RouterObject>: URLRequestConvertible {
case CreateObject(T, [String: AnyObject])
case ReadObject(T, String)
case UpdateObject(T, String, [String: AnyObject])
case DestroyObject(T, String)
var method: Alamofire.Method {
switch self {
case .CreateObject:
return .POST
case .ReadObject:
return .GET
case .UpdateObject:
return .PUT
case .DestroyObject:
return .DELETE
}
}
var path: String {
switch self {
case .CreateObject(let object, _):
return object.createObjectPath()
case .ReadObject(let object, let identifier):
return object.readObjectPath(identifier)
case .UpdateObject(let object, let identifier, _):
return object.updateObjectPath(identifier)
case .DestroyObject(let object, let identifier):
return object.destroyObjectPath(identifier)
}
}
// MARK: URLRequestConvertible
var URLRequest: NSMutableURLRequest {
let URL = NSURL(string: baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
if let token = OAuthToken {
mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
switch self {
case .CreateObject(_, let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .UpdateObject(_, _, let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
}
Example Usage
func exampleUsage() {
let URLRequest = Router.CreateObject(Location(), ["address": "1234 Road of Awesomeness"]).URLRequest
Alamofire.request(URLRequest)
.response { request, response, data, error in
print(request)
print(response)
print(data)
print(error)
}
}
Now there are certainly a few tradeoffs that you have to make here. First off, your model objects need to conform to the RouterObject
protocol. Otherwise the Router
has no idea what to use for the path. Also, you'll need to make sure all your paths can be constructed with the a single identifier
. If they cannot, this design might not work. The last issue is that you cannot store the baseURL
or the OAuthToken
directly inside the Router
enum. Unfortunately, static and stored properties are not yet supported in generic enumerations.
Regardless, this would certainly be a valid way to avoid having to create a Router
for every model object.
Should the
Alamofire.Manager.sharedInstance
be used as my singletonNetworkManager
instance?
It certainly could be used in that fashion. It really depends upon your use case and how you have designed your network access. It also depends on how many different types of sessions you need. If you need background sessions and default sessions, then you probably still need the concept of a NetworkManager
that contains each custom Manager
instance. However, if you are just hitting the network with a default session, then the sharedInstance
would probably be sufficient.
How could the
baseURL
of theAlamofire
singleton be used in conjunction with theRouter
pattern?
Good question...the code below is one example of how it could be done.
Alamofire Manager extension
extension Manager {
static let baseURLString = "http://example.com"
static var OAuthToken: String?
}
Router URLRequestConvertible Updates
var URLRequest: NSMutableURLRequest {
let URL = NSURL(string: Alamofire.Manager.baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
if let token = Alamofire.Manager.OAuthToken {
mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
switch self {
case .CreateObject(_, let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .UpdateObject(_, _, let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
Hopefully that helps shed some light. Best of luck!
Alamofire URLRequestConvertible global headers
Alamofire already generates some headers for the whole session when you using the default Session
:
let session = Session.default
or the AF
reference (which uses the default Session
behind the scenes)
Those default headers include Accept-Encoding, Accept-Language and User-Agent. As for Content-Type header, URLEncoding
or JSONEncoding
are responsible for setting the correct values. (JSONParameterEncoder
or URLEncodedFormParameterEncoder
when you passing Encodable
types as parameters)
However, you can override those headers by creating a new Session
using your own URLSessionConfiguration
:
let configuration = URLSessionConfiguration.af.default // Alamofire's default URLSessionConfiguration
configuration.headers["Content-Type"] = "application/json"
let session = Session(configuration: configuration)
Note: For Bearer token, there is a mechanism which requires the implementation of RequestInterceptor
protocol in order to intercept every request and append the correct header. Using the same protocol you can also provide a refresh token functionality.
Type does not conform to protocol 'URLRequestConvertible' with Alamofire
You need to return an NSMutableURLRequest
in the URLRequest
property instead of an NSURLRequest
. That will fix up the error.
Update
In Swift 3 and Alamofire 4, you need to return a URLRequest
from the new asURLRequest()
method. For more details, please refer to our much more detailed README examples.
Alamofire encode [[String: Any]]
Alamofire doesn't have built in support for such parameters, but you can encode them into the URLRequest
yourself before using Alamofire to issue it, or write an extension to SessionManager
that can take parameters of that form and use your own parameter encoder to create the URLRequest
.
Related Topics
How to Use Unsafemutablepointer in Swift 3
How to Convert an Pffile to an Uiimage with Swift
Uiscrollview Pauses Nstimer While Scrolling
Swift JSON Error:Could Not Cast Value of Type '_Nsdictionarym' to 'Nsarray'
Hashtags in Arabic Language Crashes the App
Uilongpressgesturerecognizer Not Calling Its Target Method
How to Get a Crash Log Due to Expiration of Provisioning Profile
Done Button Click Event in Avplayerviewcontroller
Block_Release Deallocating UI Objects on a Background Thread
Customize Apps Gallery in Appstore Page
Ipa Generated by Swift Is So Big, About 5Mb
How to Use Nsunderlinestyle.Patterndot
Present Uialertcontroller from Appdelegate
How to Determine Which Textfield Is Active Swift