How to implement multiple Routers with similar asURLRequest() - swift

I have been working to re-implement a healthy network layer in our app and Routers are noted in many tutorials / Alamofire documentation. The app has a lot of endpoints, to keep things readable I want to split them out into their subset of services. That was also noted as a best practice.
The very first endpoint that I implemented works perfectly fine, but, when I create another Router there is the asURLRequest() function which would pretty much be a duplicate. The only difference could be the switch/case. Otherwise its almost certain to be the same.
To do this in Kotlin or Java, I would create a class and extend the function calling the super. Im not certain how that works here in Swift.
enum AuthenticationRouter: URLRequestConvertible {
case login(username:String, password:String)
// MARK: - HTTPMethod
private var method: HTTPMethod {
switch self {
case .login:
return .get
}
}
// MARK: - Path
private var path: String {
switch self {
case .login:
return "users/login"
}
}
// MARK: - Parameters
private var parameters: Parameters? {
switch self {
case .login:
return nil
}
}
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try K.TestServer.baseURL.asURL()
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
// HTTP Method
urlRequest.httpMethod = method.rawValue
// Common Headers
urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
switch self {
case .login(let username, let password):
// Handle adjusting headers or request as needed
default: ()
}
// Parameters
if let parameters = parameters {
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
}
return urlRequest
}
}

Here is and old code I used. I’d change the base class to protocol instead and continue your usage of enums that conforms to that protocol I like it much better.
import Alamofire
protocol API {
var method: HTTPMethod { get }
var encoding: ParameterEncoding? { get }
var parameters: Parameters { get }
var headers: HTTPHeaders { get }
var timeout:TimeInterval { get }
var path: String { get }
var baseUrl: String { get }
}
class Router: API {
var method: HTTPMethod { .get }
var encoding: ParameterEncoding? { JSONEncoding.default }
var parameters: Parameters {
let params = Parameters()
return params
}
var headers: HTTPHeaders {
let headers = HTTPHeaders()
// TODO - add defualt headers if needed
return headers
}
var timeout: TimeInterval {
#if DEBUG
return 120
#else
return 30
#endif
}
var path: String {
fatalError("Must be overridden in subclass")
}
var baseUrl: String { Consts.serverUrl }
}
extension Router: URLRequestConvertible {
open func asURLRequest() throws -> URLRequest {
guard let baseURL = URL(string: baseUrl) else { throw someError }
let appendedURL = baseURL.appendingPathComponent(path)
let urlRequest = try URLRequest(url: appendedURL, method: method)
guard let encoder = encoding else { throw someError }
var eUrlRequest = try encoder.encode(urlRequest, with: parameters)
eUrlRequest.timeoutInterval = timeout
headers.forEach { eUrlRequest.addValue($0.value, forHTTPHeaderField: $0.name) }
return eUrlRequest
}
}
And then an example UserRouter would be
import Alamofire
final class UserRouter: Router {
enum Endpoint {
case signIn(userName: String, password: String)
}
let endpoint: Endpoint
init(endpoint: Endpoint) {
self.endpoint = endpoint
}
override var method: HTTPMethod {
.get
}
override var parameters: Parameters {
var params = super.parameters
switch endpoint {
case .signIn(let userName, let password):
params[“login”] = [“username”: userName, “password”: password]
}
return params
}
override var path: String {
switch endpoint {
case .signIn:
return "/signIn"
}
}
}
In this way the asUrlRequest() function will be used throughout the code (; the Alamofire.Session().request() function will accept the each one of the routers.
Additionally, there is a great open-source abstraction that does similar work above Alamofire. Moya - https://github.com/Moya/Moya.
It supports Combine and RxSwift right out of the box

Related

How add page for pagination in protocol oriented networking

I just learn how to create a protocol oriented networking, but I just don't get how to add page for pagination in protocol. my setup is like this
protocol Endpoint {
var base: String { get }
var path: String { get }
}
extension Endpoint {
var apiKey: String {
return "api_key=SOME_API_KEY"
}
var urlComponents: URLComponents {
var components = URLComponents(string: base)!
components.path = path
components.query = apiKey
return components
}
var request: URLRequest {
let url = urlComponents.url!
return URLRequest(url: url)
}
}
enum MovieDBResource {
case popular
case topRated
case nowPlaying
case reviews(id: Int)
}
extension MovieDBResource: Endpoint {
var base: String {
return "https://api.themoviedb.org"
}
var path: String {
switch self {
case .popular: return "/3/movie/popular"
case .topRated: return "/3/movie/top_rated"
case .nowPlaying: return "/3/movie/now_playing"
case .reviews(let id): return "/3/movie/\(id)/videos"
}
}
}
and this is my network service class method
func getReview(movie resource: MovieDBResource, completion: #escaping (Result<MovieItem, MDBError>) -> Void) {
print(resource.request)
fetch(with: resource.request, decode: { (json) -> MovieItem? in
guard let movieResults = json as? MovieItem else { return nil }
return movieResults
}, completion: completion)
}
How I add page in protocol so I can called and add parameter in viewController, for now my service in my viewController is like this. I need parameter to page
service.getReview(movie: .reviews(id: movie.id)) { [weak self] results in
guard let self = self else { return }
switch results {
case .success(let movies):
print(movies)
case .failure(let error):
print(error)
}
}
Thank you
You can add it as a parameter to your enum case like:
case popular(queryParameters: [String: Any])
You can create Router class that has buildRequest(_:Endpoint), and here you can get the queryParameters and add it to the url witch will contain page and limit or any other query parameters.
you can also add another parameter for bodyParameters if the request HTTPMethod is POST and you need to send data in the body.

Error when tried to translate simple Alamofire request to Moya

I am trying to improve Network layer, so I used Moya in order to create a more scalable code.
I have a simple request method (with Alamofire) that requests a JSON object (I'm using this theMovieDB api as an example. So, this is the working code:
class Endpoints{
func getGenres(callback: #escaping (NSDictionary?)->()){
let headers = [
"Authorization": "Bearer 1234556789123345",
"Content-Type": "application/json;charset=utf-8"
]
let parameters: [String: String] = ["api_key": "1234567890"]
let url = "https://api.themoviedb.org/3/genre/movie/list?"
Alamofire.request(url, method: .get, parameters: parameters, headers: headers).responseJSON{ (response:DataResponse<Any>) in
switch(response.result) {
case .success(_):
if response.result.value != nil{
let t: NSDictionary = response.result.value as! NSDictionary
callback(t)
}
break
case .failure(_):
print(response.result.error!)
callback(nil)
break
}
}
}
The code above works totally fine, but I want to code the Moya equivalent structure, so I have this, strongly inspired in the documentation:
import Foundation
import Moya
enum MyService {
case getGenres
}
// MARK: - TargetType Protocol Implementation
extension MyService: TargetType {
var baseURL: URL { return URL(string:
"https://api.themoviedb.org/3")! }
var path: String {
switch self {
case .getGenres:
return "/genre/movie/list?"
}
}
var method: Moya.Method {
switch self {
case .getGenres:
return .get
}
}
var parameters: [String: Any]? {
switch self {
case .getGenres:
return ["api_key": "1234567890"]
}
}
var parameterEncoding: ParameterEncoding {
switch self {
case .getGenres:
return JSONEncoding.default
}
}
var sampleData: Data {
switch self {
case .getGenres:
return "".utf8Encoded
}
}
var task: Task {
switch self {
case .getGenres:
return .request
}
}
var headers: [String: String]? {
return [
"Authorization": "Bearer 1234556789123345",
"Content-Type": "application/json;charset=utf-8"
]
}
}
// MARK: - Helpers
private extension String {
var urlEscaped: String {
return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
}
var utf8Encoded: Data {
return self.data(using: .utf8)!
}
}
When using Moya service:
let provider = MoyaProvider<MyService>()
provider.request(.getGenres) { (result) in
print(result)
}
I always get a 504 or timeout response when using Moya services.
Any ideas?

Swith enum can't inherit methods, best way to prevent rewriting same function for every enum

So I'm writing my networking code using a router design pattern. I'm writing a new router for different components of my app (should i be doing this? I try to limit my objects lines of code). Heres my router enum. If I was using a class, I could define a method once to populate variables like HTTPMethod and override them if necessary. Is there a way to do this with enums? Is it worth implementing or should i repeat the same code. There are a few other places besides httpMethod such as URL construction where I think this could be helpful.
I was thinking i could do something with protocols but am not sure if I'm wasting my time.
enum PRRouter: URLRequestConvertible {
static let baseURLString = "http://localhost:8000/"
case get(Int)
case create([String : Any])
case delete(Int)
func asURLRequest() throws -> URLRequest {
var method: HTTPMethod{
switch self {
case .get:
return .get
case .create:
return .post
case.delete:
return .delete
}
}
let params : ([String : Any]?) = {
switch self {
case .get, .delete:
return nil
case .create(let newTodo):
return newTodo
}
}()
let url : URL = {
let relativePath: String?
switch self{
case .get(let number):
relativePath = "test/\(number)"
case .create:
relativePath = "test/"
case .delete:
relativePath = "test/"
}
var url = URL(string: PRRouter.baseURLString)!
if let relativePath = relativePath {
url = url.appendingPathComponent(relativePath)
}
return url
}()
var urlRequest = URLRequest(url:url)
urlRequest.httpMethod = method.rawValue
let encoding = JSONEncoding.default
return try encoding.encode(urlRequest, with: params)
}
Make the enum conform to a protocol with a default implementation.
protocol P {
func f()
}
extension P {
func f() { print("default implementation") }
}
enum E: P {
case Foo
}
let e = E.Foo
e.f()
I do something similar in my own project. Here is an example based on your code to get you started:
protocol APIProtocol {
var path: String { get }
var method: HTTPMethods { get }
var bodyParameters: [String: Any?]? { get }
}
enum HTTPMethods: String {
case get = "GET"
case post = "POST"
}
enum PRRouter: APIProtocol {
case get(Int)
case create([String : Any])
case delete(Int)
var path: String {
switch self {
case let .get(number):
return "test/\(number)"
default:
return "test"
}
}
var method: HTTPMethods {
return .get
}
var bodyParameters: [String : Any?]? {
return nil
}
}
extension APIProtocol {
func execute(completion: #escaping ((Data?) -> Void)) -> URLSessionDataTask? {
guard let url = URL(string: "http://localhost:8000/\(path)") else { return nil }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
if let bodyParameters = bodyParameters {
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: bodyParameters, options: [.prettyPrinted])
}
let task = URLSession.shared.dataTask(with: urlRequest) { (data, urlResponse, error) in
completion(data)
}
task.resume()
return task
}
}
Finally you can use it like this:
let dataTask = PRRouter.get(2).execute { (data) in
//
}
You could extend this further by changing the completion block in the execute function to return a deserialized object.

Swift Moya sending parameters as JSON in request body

This is how my Endpoint looks:
extension MyEndpoint: TargetType {
var baseURL: NSURL { return NSURL(string: "http://10.0.1.13:5000")! }
var path: String {
switch self {
case .SearchForNodes(_, _, _):
return "/api/search/node/"
case .CreateHistoricalEvent(_):
return "/api/node/historicalevent/"
}
}
var method: Moya.Method {
switch self {
case .SearchForNodes(_, _, _):
return .GET
case .CreateHistoricalEvent(_):
return .POST
}
}
var parameters: [String: AnyObject]? {
switch self {
case .SearchForNodes(let startsWith, let page, let pageSize):
return ["startsWith": startsWith, "page": page, "pageSize": pageSize]
case .CreateHistoricalEvent(let title):
return ["Title": title]
}
}
var parameterEncoding: Moya.ParameterEncoding {
switch self {
case .CreateHistoricalEvent:
return ParameterEncoding.Json
default:
return ParameterEncoding.URL
}
}
}
Now I want to make my CreateHistoricalEvent method to post its parameters as JSON in request body. What am I missing?
In Swift3
Change:
var parameterEncoding: Moya.ParameterEncoding {
switch self {
case .CreateHistoricalEvent:
return ParameterEncoding.Json
default:
return ParameterEncoding.URL
}
}
To:
var parameterEncoding: Moya.ParameterEncoding {
return JSONEncoding.default
}
If you want to send data with JsonArray, try it:
import Alamofire
struct JsonArrayEncoding: Moya.ParameterEncoding {
public static var `default`: JsonArrayEncoding { return JsonArrayEncoding() }
/// Creates a URL request by encoding parameters and applying them onto an existing request.
///
/// - parameter urlRequest: The request to have parameters applied.
/// - parameter parameters: The parameters to apply.
///
/// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
///
/// - returns: The encoded request.
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var req = try urlRequest.asURLRequest()
let json = try JSONSerialization.data(withJSONObject: parameters!["jsonArray"]!, options: JSONSerialization.WritingOptions.prettyPrinted)
req.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
req.httpBody = json
return req
}
}
and
var parameterEncoding: Moya.ParameterEncoding {
switch self {
case .CreateHistoricalEvent:
return JsonArrayEncoding.default
default:
return JSONEncoding.default
}
}
extension MoyaProvider {
public class func JSONEndpointMapping(target: Target) -> Endpoint<Target> {
let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
return Endpoint(
URL: url,
sampleResponseClosure: {
.NetworkResponse(200, target.sampleData)
},
method: target.method,
parameters: target.parameters,
parameterEncoding: Moya.ParameterEncoding.JSON
)
}
}
let DemoAPI = MoyaProvider<DemoTarger>(endpointClosure: MoyaProvider.JSONEndpointMapping)

Refactoring Code in Swift

I have a pretty standard problem here that I am a little confused how to make more elegant in the swift tradition of things.
I have a base URL string in my APIClient, a path for different methods to use, and methods might pass in a optional string.
How do I write this such that it is much cleaner? I feel like I am writing swift code but not using any of the new constructs (like guard, let), etc.
// call the API
ApiClient.sharedInstance.getUser("56cfffce227a6c2c9b000001", successCompletion: successCompletion, failureCompletion: failureCompletion)
// setting the current URL
let currentURL = String("http://localhost:3000")
// class definition
class ApiClient {
var baseURL:String!;
// have to init the string class for the
init (base:String){
self.baseURL = base
}
class var sharedInstance: ApiClient {
struct Static{
static let instance = ApiClient(base: currentURL)
}
return Static.instance
}
func getUser(user_id: String?, successCompletion: ResultHandler<User>, failureCompletion:ResultHandler<FailureReason>){
// this is awkward
var url: URLStringConvertible!
// this is even more awkward
if user_id != nil {
url = "\(currentURL)/users/\(user_id!).json"
} else {
url = "\(currentURL)/users/me"
}
}
}
Cleaned some things up and removed some unnecessary forced-unwraps.
class ApiClient {
var baseURL: String;
// have to init the string class for the
init (base:String){
self.baseURL = base
}
class var sharedInstance: ApiClient {
struct Static{
static let instance = ApiClient(base: currentURL)
}
return Static.instance
}
func getUser(user_id: String?, successCompletion: ResultHandler<User>, failureCompletion:ResultHandler<FailureReason>){
var url: URLStringConvertible
if let user_id = user_id {
url = "\(currentURL)/users/\(user_id!).json"
}
else{
url = "\(currentURL)/users/me"
}
}
Depending on what you're doing in getUser, things could be done differently.
FYI, what I tend to do for singletons: (arguably not the best):
private static var realSharedInstance: Class?
static var sharedInstance: Class {
get {
if let realSharedInstance = realSharedInstance {
return realSharedInstance
}
else{
realSharedInstance = Class()
return realSharedInstance!
}
}
}
For me, I like to create an Endpoint for every endpoint that the API supports. In this case, a UsersEndpoint which can make requests via APIClient.
This is easier to scale (for me) when there more endpoints and each endpoint has multiple resources.
Use in code:
MyAPI.sharedInstance.users.getUser(id, parameters: params, success: {}, failure: {})
MyAPI.sharedInstance.anotherEndpoint.doSomething(parameters: params, success: {}, failure: {})
The classes:
class MyAPI {
static let sharedInstance = MyAPI()
let users: UsersEndpoint
// more endpoints here
init() {
users = UsersEndpoint()
// ....
}
}
class APIClient {
static let sharedClient = APIClient()
var manager : Alamofire.Manager!
var baseURL: String
init() {
baseURL = String(format: "https://%#%#", self.instanceUrl, API.Version)
}
func get(endpoint endpoint: String,
parameters: [String:AnyObject],
success: SuccessCallback,
failure: FailureCallback) {
let requestUrl = NSURL(string: "\(self.baseURL)\(endpoint)")!
manager.request(.GET, requestUrl, parameters: parameters, headers: headers)
.validate()
.responseData { response in
switch response.result {
case .Success(let value):
if let data: NSData = value {
success(data: data)
log.verbose("Success - GET requestUrl=\(requestUrl)")
}
break;
case .Failure(let error):
failure(error: error)
log.error("Failure - GET requestUrl=\(requestUrl)\nError = \(error.localizedDescription)")
break;
}
}
}
}
class APIFacade {
var endpoint: String
init(endpoint: String) {
self.endpoint = endpoint;
}
func get(endpoint endpoint: String,
parameters: [String:AnyObject],
success: SuccessCallback,
failure: FailureCallback) {
APIClient.sharedClient.get(endpoint: endpoint, parameters: parameters, success: success, failure: failure)
}
}
class UsersEndpoint: APIFacade {
init() {
super.init(endpoint: "/users")
}
func getUser(id: String, parameters: [String:AnyObject], success: SuccessCallback, failure: FailureCallback) {
super.get(endpoint: "\(self.endpoint)/\(id)", parameters: parameters, success: success, failure: failure)
}
}