Singleton pattern and proper use of Alamofire's URLRequestConvertible - swift

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.

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!

Related

What is the type of struct.self in swift?

I have a structural variable that extends Codable protocol, and want to memorize its type, so that I can use it next time in JSONDecoder. However, when a private variable is declared, its class needs to be specified, but whatever class I'm trying use, I cannot use the result later in JSONDecoder. So my question is what is the type of myVariable.self?
I have this problem, because I want to specify the class when a view decoder is initialized. During the initialization, I call the following function (this part of the code works well):
func getData<T: Codable>(fromURL: String, onSuccess: #escaping (T) -> Void, onError: #escaping (String) -> Void) {
let url = URL(string: fromURL)!
let task = session.dataTask(with: url) {(data, response, error) in
DispatchQueue.main.async {
if let error = error {
onError(error.localizedDescription)
return
}
guard let data = data, let response = response as? HTTPURLResponse else {
onError("Invalid data or response")
return
}
do {
if response.statusCode == 200 {
let ret = try JSONDecoder().decode(T.self, from: data)
onSuccess(ret)
} else {
let err = try JSONDecoder().decode(APIError.self, from: data)
onError(err.message)
}
} catch {
onError(error.localizedDescription)
}
}
}
task.resume()
}
This part of the code works well. However, if I need to upload more data, I need to know T.self for proper decoder, so I need to keep it. A solution could be to create a private variable:
private var type: ??? // what type should I put here?
and to write in the function's body
type = T.self
But whichever type I try, it doesn't work.
What type should I put there? Or, maybe, there are some other solutions?
You can declare the property
private var type: Codable.Type
or wrap the method in a generic struct or class and declare the property
class CodableWrapper<T : Codable> {
private var type: T.Type
...

Cannot convert value of type 'MyEnum<T.Type>' to expected argument type 'MyEnum<_>'

I have a network layer working with generics and I'm using protocols so I can test it later. I have followed this tutorial https://medium.com/thecocoapps/network-layer-in-swift-4-0-972bf2ea5033
This is my Mock for testing:
import Foundation
#testable import TraktTest
class MockUrlSessionProvider: ProviderProtocol {
enum Mode {
case success
case empty
case fail
}
private var mode: Mode
init(mode: Mode) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
switch mode {
case .success: completion(NetworkResponse.success(T))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}
I'm getting the error: Cannot convert value of type 'NetworkResponse<T.Type>' to expected argument type 'NetworkResponse<_>' in this line: completion(NetworkResponse.success(T))
If I send this to my completion success it compile:
try? JSONDecoder().decode(T.self, from: data!)
(dummy data that I created using encode and my model), but crash when get to my model because is nil despite I had encoded using JSONEncoder() with a correct model.
I think it works, because is the same logic that I use in my class that implements ProviderProtocol in my app:
final class URLSessionProvider: ProviderProtocol {
private var session: URLSessionProtocol
init(session: URLSessionProtocol = URLSession.shared) {
self.session = session
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
let request = URLRequest(service: service)
session.dataTask(request: request) { [weak self] data, response, error in
let httpResponse = response as? HTTPURLResponse
self?.handleDataResponse(data: data, response: httpResponse, error: error, completion: completion)
}.resume()
}
private func handleDataResponse<T: Decodable>(data: Data?, response: HTTPURLResponse?, error: Error?, completion: (NetworkResponse<T>) -> Void) {
guard error == nil else { return completion(.failure(.unknown(error?.localizedDescription ?? "Error"))) }
guard let response = response else { return completion(.failure(.unknown("no_response".localized()))) }
switch response.statusCode {
case 200...299:
guard let data = data, let model = try? JSONDecoder().decode(T.self, from: data) else { return completion(.failure(.noData)) }
completion(.success(model))
default: completion(.failure(.unknown("no_response".localized())))
}
}
}
URLSessionProtocol is just a protocol which has a method dataTask same as the one in URLSession.shared (receive a URLRequest and returns Data, Response and Error in a completion).
My Network responses are a couple of enums:
enum NetworkResponse<T> {
case success(T)
case failure(NetworkError)
}
enum NetworkError {
case unknown(String)
case noData
}
My provider protocol just have a function to make the request using generics:
protocol ProviderProtocol {
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping(NetworkResponse<T>) -> Void) where T: Decodable
}
I don't think I need to use ServiceProtocol in my test because is to setup the request with endpoint, headers, body, id, etc. But this is the protocol I created:
typealias Headers = [String: String]
typealias Parameters = [String: Any]
protocol ServiceProtocol {
func baseURL() -> URL
var path: String? { get }
var id: String? { get }
var method: HTTPMethod { get }
var task: Task { get }
var headers: Headers? { get }
var parametersEncoding: ParametersEncoding { get }
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
enum Task {
case requestPlain
case requestParameters(Parameters)
}
enum ParametersEncoding {
case url
case json
}
In my app, I have a class that implement ProviderProtocol and use a URLSession.shared to make the dataTask when some viewModel call the request with the appropiate model.
I'm use to make test with protocols and a specific model, but with generics is showing me that error. How can I achieve to have a mock provider using generics so I can test any viewModel who make a call to network using different kinds of models (stubs).
The error occurs because NetworkResponse expects an instance of T, while the mock tries to provide the actual T.
So, you need to somehow provide an instance, however this cannot be generated by the mock as it doesn't have enough information about how to construct an instance.
I recommend injecting the success value from the outside, when creating the mock. You can do this either by making the mock class generic, or by making the Mode enum generic. Below is a sample implementation for the latter:
class MockUrlSessionProvider: ProviderProtocol {
// making the enum generic, to support injecting the success value
enum Mode<T> {
case success(T)
case empty
case fail
}
// need to have this as `Any` to cover all possible T generic arguments
private var mode: Any
// however the initializer can be very specific
init<T>(mode: Mode<T>) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
// if the mock was not properly configured, do nothing
guard let mode = mode as? Mode<T> else { return }
// alternatively you force cast and have the unit test crash, this should help catching early configuration issues
// let mode = mode as! Mode<T>
switch mode {
case let .success(value): completion(NetworkResponse.success(value))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}

Is there a better way to build many different URLs?

I am working on building a framework to connect to a specific API and will need to build a lot of different paths. My current setup is using an Enum for returning a URL, which works pretty well for the most part. My only problem I have with this approach is there will be a lot of different cases (~30 total) by the time I'm done. I was wondering if anyone has a better solution?
enum API {
var baseURL: URL {
return URL(string: "https://api.example.com")!
}
case user
case emails
case posts
case post(id: String)
// etc . . .
}
extension API: Path {
func appendPathComponent(_ string: String) -> URL {
return baseURL.appendingPathComponent(string)
}
var url: URL {
switch self {
case .user: return baseURL
case .emails: return appendPathComponent("email")
case .posts: return appendPathComponent("posts")
case .post(let id): return appendPathComponent(id)
// etc
}
}
}
// call site
let url = API.emails.url
I would turn this around the same way that Notification.Name does. Make these extensions on URL:
extension URL {
static let apiBase = URL(string: "https://api.example.com")!
static let apiUser = apiBase
static let apiEmails = apiBase.appendingPathComponent("email")
static let apiPosts = apiBase.appendingPathComponent("posts")
static func apiPost(id: String) -> URL { return apiBase.appendingPathComponent(id) }
}
Then calling it is just:
let x = URL.apiEmails
And in cases where URL is known, you don't even have to include that:
let task = URLSession.shared.dataTask(with: .apiEmails)
or
let task = URLSession.shared.dataTask(with: .apiPost(id: "123"))

Swift: Pass type from property to generic function

For my networking module, I have this protocol that I adopt for accessing different parts of the API:
protocol Router: URLRequestConvertible {
var baseUrl: URL { get }
var route: Route { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var encoding: ParameterEncoding? { get }
var responseResultType: Decodable.Type? { get }
}
I'm adopting this with enums that look like this:
enum TestRouter: Router {
case getTestData(byId: Int)
case updateTestData(byId: Int)
var route: Route {
switch self {
case .getTestData(let id): return Route(path: "/testData/\(id)")
case .updateTestData(let id): return Route(path: "/testDataOtherPath/\(id)")
}
}
var method: HTTPMethod {
switch self {
case .getTestData: return .get
case .updateTestData: return .put
}
}
var headers: [String : String]? {
return [:]
}
var encoding: ParameterEncoding? {
return URLEncoding.default
}
var responseResultType: Decodable.Type? {
switch self {
case .getTestData: return TestData.self
case .updateTestData: return ValidationResponse.self
}
}
}
I want to use Codable for decoding nested Api responses. Every response consists of a token and a result which content is depending on the request route.
For making the request I want to use the type specified in the responseResultType property in the enum above.
struct ApiResponse<Result: Decodable>: Decodable {
let token: String
let result: Result
}
extension Router {
func asURLRequest() throws -> URLRequest {
// Construct URL
var completeUrl = baseUrl.appendingPathComponent(route.path, isDirectory: false)
completeUrl = URL(string: completeUrl.absoluteString.removingPercentEncoding ?? "")!
// Create URL Request...
var urlRequest = URLRequest(url: completeUrl)
// ... with Method
urlRequest.httpMethod = method.rawValue
// Add headers
headers?.forEach { urlRequest.addValue($0.value, forHTTPHeaderField: $0.key) }
// Encode URL Request with the parameters
if encoding != nil {
return try encoding!.encode(urlRequest, with: route.parameters)
} else {
return urlRequest
}
}
func requestAndDecode(completion: #escaping (Result?) -> Void) {
NetworkAdapter.sessionManager.request(urlRequest).validate().responseData { response in
let responseObject = try? JSONDecoder().decode(ApiResponse<self.responseResultType!>, from: response.data!)
completion(responseObject.result)
}
}
}
But in my requestAndDecode method It throws an compiler error (Cannot invoke 'decode' with an argument list of type '(Any.Type, from: Data)'). I can't use ApiResponse<self.responseResultType!> like that.
I could make this function generic and call it like this:
TestRouter.getTestData(byId: 123).requestAndDecode(TestData.self, completion:)
but then I'd have to pass the response type everytime I want to use this endpoint.
What I want to achieve is that the extension function requestAndDecode takes it response type information from itself, the responseResultType property.
Is this possible?
Ignoring the actual error report you have a fundamental problem with requestAndDecode: it is a generic function whose type parameters are determined at the call site which is declared to return a value of type Result yet it attempts to return a value of type self.responseResultType whose value is an unknown type.
If Swift's type system supported this it would require runtime type checking, potential failure, and your code would have to handle that. E.g. you could pass TestData to requestAndDecode while responseResultType might be ValidationResponse...
Change the JSON call to:
JSONDecoder().decode(ApiResponse<Result>.self ...
and the types statically match (even though the actual type that Result is is unknown).
You need to rethink your design. HTH
Create a Generic function with Combine and AlomFire. You can use it for all method(get, post, put, delete)
func fatchData<T: Codable>(requestType: String, url: String, params: [String : Any]?, myType: T.Type, completion: #escaping (Result<T, Error>) -> Void) {
var method = HTTPMethod.get
switch requestType {
case "Get":
method = HTTPMethod.get
case "Post":
method = HTTPMethod.post
print("requestType \(requestType) \(method) ")
case "Put":
method = HTTPMethod.put
default:
method = HTTPMethod.delete
}
print("url \(url) \(method) \(AppConstant.headers) ")
task = AF.request(url, method: method, parameters: params, encoding: JSONEncoding.default, headers: AppConstant.headers)
.publishDecodable(type: myType.self)
.sink(receiveCompletion: { (completion) in
switch completion{
case .finished:
()
case .failure(let error):
// completion(.failure(error))
print("error \(error)")
}
}, receiveValue: {
[weak self ](response) in
print("response \(response)")
switch response.result{
case .success(let model):
completion(.success(model))
print("error success")
case .failure(let error):
completion(.failure(error))
print("error failure \(error.localizedDescription)")
}
}
)
}

Set default baseURLString using manager session configuration

I'm developing an IOS app using swift with Alamofire library. Two files of my network model are described below, the first one is the ApiClient Class with the Alamofire manager session configuration and 2 instances of the services,in my question I'm only describing one, the Service1 struct, in this struct I'm using the Alamofire Router to request to the REST API, and I would like to know, How can I set a default baseURLString ("https://api.com/v1") using manager session configuration? instead of declare it inside each struct Service.
ApiClient.swift
class ApiClient {
var OAuthToken = "ZMpMDJhB0egjFIzFhapuWNFSBhX2conQ1e+3vlv0XrJQVcw7fRg=="
var service1: Service1
var service2: Service2
init() {
let manager = Manager.sharedInstance
manager.session.configuration.HTTPAdditionalHeaders = [
"Authorization": "Bearer \(OAuthToken)"
]
service1 = Service1()
service2 = Service2()
}
}
Service1.swift
struct Service1 {
enum Router: URLRequestConvertible {
static let baseURLString = "https://api.com/v1"
case GetList([String: AnyObject])
case GetById(String)
case Add([String: AnyObject])
case Update(String, [String: AnyObject])
case Delete(String)
case GetListFromPoint([String: AnyObject])
var method: Alamofire.Method {
switch self {
case .GetList:
return .GET
case .GetById:
return .GET
case .Add:
return .POST
case .Update:
return .PUT
case .Delete:
return .DELETE
case .GetListFromPoint:
return .GET
}
}
var path: String {
switch self {
case .GetList:
return "/endPoint"
case .GetById(let id):
return "/endPoint/\(id)"
case .Add:
return "/endPoint"
case .Update(let id, _):
return "/endPoint/\(id)"
case .Delete(let id):
return "/endPoint/\(id)"
case .GetListFromPoint:
return "/near/endPoint"
}
}
var URLRequest: NSURLRequest {
let URL = NSURL(string: Router.baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
switch self {
case .GetList(let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
case .Add(let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .Update(_, let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .GetListFromPoint(let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
}
func getList(completionHandler: ([Object1]?, NSError?) -> ()) {
Alamofire.request(Router.GetList(["skip": 0, "limit": 100])).responseJSON { (request, response, json, error) in
//Converting json to object
}
func getById(completionHandler: ([Object1]?, NSError?) -> ()) {
Alamofire.request(Router.GetById(id)).responseJSON { (request, response, json, error) in
//Converting json to object
}
}
I think there are MANY ways you could do this. Here are a few ideas...
Store the baseURLString in the APIClient and pass the value into each of the Service initializers
Create a BaseService class where you store all common types of information between the services such as baseURLString, additional headers, etc.
Expose the baseURLString in the APIClient and pass a weak reference of the APIClient into each service.
Create a separate object such as an AppEnvironment that stores properties such as OAuth info, baseURLString, etc. that can be accessed through either a global singleton or class properties.
And there are many more ways. Hopefully one of those ideas triggers your imagination.