Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Singleton pattern and proper use of Alamofire's URLRequestConvertible

This is a 2 part question the first is similar to this question here: Proper usage of the Alamofire's URLRequestConvertible. But I need a little more help!

1) Do I create an enum router which implements URLRequestConvertible for each model in my model layer?

The alamofire github page provides an example of a router which i've copied here:

  enum Router: URLRequestConvertible {
    static let baseURLString = "http://example.com"
    static var OAuthToken: String?

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

    var method: Alamofire.Method {
        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

    var URLRequest: NSURLRequest {
        let URL = NSURL(string: Router.baseURLString)!
        let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
        mutableURLRequest.HTTPMethod = method.rawValue

        if let token = Router.OAuthToken {
            mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }

        switch self {
        case .CreateUser(let parameters):
            return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
        case .UpdateUser(_, let parameters):
            return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
        default:
            return mutableURLRequest
        }
    }
}

When I look at this (i'm new at swift so please bear with me >_<) I see operations on a user object; they are creating a user, updating a user etc... So, if I had model objects person, company, location in my model layer, would I create a router for each model object?

2) When interacting heavily with an API, I'm used to creating a "network manager" singleton to abstract away the network layer and to hold headers and the baseurl for that API. The alamofire has a "Manager" described here:

Top-level convenience methods like Alamofire.request use a shared instance of Alamofire.Manager, which is configured with the default NSURLSessionConfiguration. As such, the following two statements are equivalent:

Alamofire.request(.GET, "http://httpbin.org/get")

let manager = Alamofire.Manager.sharedInstance
manager.request(NSURLRequest(URL: NSURL(string: "http://httpbin.org/get")))

is this manager what I should be using as my singleton? If so, how do I set the baseurl on the manager? Also, if I use this manager does / can this work together with the router construct shown above (with each model object setting it's baseurl and NSURLRquest)? If so can you provide a simple example?

I'm new to the Alamofire library and swift. So, I know there are a lot of holes in my understanding but I'm just trying to understand the best that I can! Any info helps. Thanks.

like image 857
SnoopyProtocol Avatar asked Jul 08 '15 07:07

SnoopyProtocol


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.

How do I pass Alamofire header in Swift?

For headers that change from request to request, you can pass them directly to the request method. From the docs: Adding a custom HTTP header to a Request is supported directly in the global request method. This makes it easy to attach HTTP headers to a Request that can be constantly changing.

What is Alamofire interceptor?

Alamofire provides Session , which is similar to URLSession in terms of responsibility. It helps create and manage different requests and provides common functionality for all requests, such as interception, response caching and more. You'll learn more about different types of requests later in this tutorial.


1 Answers

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 singleton NetworkManager 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 the Alamofire singleton be used in conjunction with the Router 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!

like image 189
cnoon Avatar answered Oct 23 '22 12:10

cnoon