Error when tried to translate simple Alamofire request to Moya - swift

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?

Related

How to implement multiple Routers with similar asURLRequest()

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

How can I use Alamofire Router to organize the API call? [swift/ Alamofire5]

I'm trying to convert my AF request to Router structures for a cleaner project. I'm getting an error for:
Value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols.
Please help me to fix the code. THANK YOU!
My URL will have a placeholder for username and the password will be sent in body. The response will be Bool (success), username and bearer token.
Under is my AF request:
let username = usernameTextField.text
let password = passwordTextField.text
let loginParams = ["password":"\(password)"]
AF.request("https://example.com/users/\(username)/login",
method: .post,
parameters: loginParams,
encoder: JSONParameterEncoder.default,
headers: nil, interceptor: nil).response { response in
switch response.result {
case .success:
if let data = response.data {
do {
let userLogin = try JSONDecoder().decode(UsersLogin.self, from: data)
if userLogin.success == true {
defaults.set(username, forKey: "username")
defaults.set(password, forKey: "password")
defaults.set(userLogin.token, forKey: "token")
print("Successfully get token.")
} else {
//show alert
print("Failed to get token with incorrect login info.")
}
} catch {
print("Error: \(error)")
}
}
case .failure(let error):
//show alert
print("Failed to get token.")
print(error.errorDescription as Any)
}
}
What I have so far for converting to AF Router structures:
import Foundation
import Alamofire
enum Router: URLRequestConvertible {
case login(username: String, password: String)
var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
var path: String {
switch self {
case .login(let username):
return "/users/\(username)/login"
}
}
var parameters: Parameters? {
switch self {
case .login(let password):
return ["password": password]
}
}
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try Constants.ProductionServer.baseURL.asURL()
var request = URLRequest(url: url.appendingPathComponent(path))
// HTTP Method
request.httpMethod = method.rawValue
// Common Headers
request.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
request.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
// Parameters
switch self {
case .login(let password):
request = try JSONParameterEncoder().encode(parameters, into: request) //where I got the error
}
return request
}
}
class APIClient {
static func login(password: String, username: String, completion: #escaping (Result<UsersLogin, AFError>) -> Void) {
AF.request(Router.login(username: username, password: password)).responseDecodable { (response: DataResponse<UsersLogin, AFError>) in
completion(response.result)
}
}
}
LoginViewController Class (where I replaced the AF.request code)
APIClient.login(password: password, username: username) { result in
switch result {
case .success(let user):
print(user)
case .failure(let error):
print(error.localizedDescription)
}
Codable UsersLogin model
struct UsersLogin: Codable {
let success: Bool
let username: String
let token: String?
enum CodingKeys: String, CodingKey {
case success = "success"
case username = "username"
case token = "token"
}
}
Took me a while but finally fixed it. I also clean up the code too.
enum Router: URLRequestConvertible {
case login([String: String], String)
var baseURL: URL {
return URL(string: "https://example.com")!
}
var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
var path: String {
switch self {
case .login(_, let username):
return "/users/\(username)/login"
}
}
func asURLRequest() throws -> URLRequest {
print(path)
let urlString = baseURL.appendingPathComponent(path).absoluteString.removingPercentEncoding!
let url = URL(string: urlString)
var request = URLRequest(url: url!)
request.method = method
switch self {
case let .login(parameters, _):
request = try JSONParameterEncoder().encode(parameters, into: request)
}
return request
}
}
Usage
let username = usernameTextField.text
AF.request(Router.login(["password": password], username)).responseDecodable(of: UsersLogin.self) { (response) in
if let userLogin = response.value {
switch userLogin.success {
case true:
print("Successfully get token.")
case false:
print("Failed to get token with incorrect login info.")
}
} else {
print("Failed to get token.")
}
}
I solved a similar problem in this way. I created a protocol Routable
enum EncodeMode {
case encoding(parameterEncoding: ParameterEncoding, parameters: Parameters?)
case encoder(parameterEncoder: ParameterEncoder, parameter: Encodable)
}
protocol Routeable: URLRequestConvertible {
var baseURL: URL { get }
var path: String { get }
var method: HTTPMethod { get }
var encodeMode: EncodeMode { get }
}
extension Routeable {
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = baseURL.appendingPathComponent(path)
var urlRequest: URLRequest
switch encodeMode {
case .encoding(let parameterEncoding, let parameters):
urlRequest = try parameterEncoding.encode(URLRequest(url: url), with: parameters)
case .encoder(let parameterEncoder, let parameter):
urlRequest = URLRequest(url: url)
urlRequest = try parameterEncoder.encode(AnyEncodable(parameter), into: urlRequest)
}
urlRequest.method = method
return urlRequest
}
}
And my routers look like this one
enum WifiInterfacesRouter: Routeable {
case listActive(installationId: Int16?)
case insert(interface: WifiInterface)
var encodeMode: EncodeMode {
switch self {
case .listActive(let installationId):
guard let installationId = installationId else {
return .encoding(parameterEncoding: URLEncoding.default, parameters: nil)
}
return .encoding(parameterEncoding: URLEncoding.default, parameters: ["idInstallation": installationId])
case .insert(let interface):
return .encoder(parameterEncoder: JSONParameterEncoder.default, parameter: interface)
}
}
var baseURL: URL {
return URL(string: "www.example.com/wifiInterfaces")!
}
var method: HTTPMethod {
switch self {
case .listActive: return .get
case .insert: return .post
}
}
var path: String {
switch self {
case .listActive: return "listActive"
case .insert: return "manage"
}
}
}
To solve the build error
Protocol 'Encodable' as a type cannot conform to the protocol itself
I used the useful AnyCodable library. A type erased implementation of Codable.
You can't use Parameters dictionaries with Encodable types, as a dictionary of [String: Encodable] is not Encodable, like the error says. I suggest moving that step of the asURLRequest process into a separate function, such as:
func encodeParameters(into request: inout URLRequest) {
switch self {
case let .login(parameters):
request = try JSONParameterEncoder().encode(parameters, into: request)
}
}
Unfortunately this doesn't scale that well for routers with many routes, so I usually break up my routes into small enums and move my parameters into separate types which are combined with the router to produce the URLRequest.

How can I write a generic wrapper for alamofire request in swift?

How can I write a generic wrapper for alamofire request ?
How can I convert the POST and GET function to Generic function in the following code?
I need to have generic request functions that show different behaviors depending on the type of data received.
Also, Can the response be generic?
My non-generic code is fully outlined below
import Foundation
import Alamofire
import SwiftyJSON
// for passing string body
extension String: ParameterEncoding {
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var request = try urlRequest.asURLRequest()
request.httpBody = data(using: .utf8, allowLossyConversion: false)
return request
}
}
public class ConnectionManager {
func Post (FirstName: String, LastName: String, NationalID: String, NationalCode: String, BirthDate: Date,Image: String,isFemale: Bool,Age:Int64,Avg:Double, completion: #escaping CompletionHandler) {
let body: [String:Any] = [
"FirstName":FirstName,
"LastName": LastName,
"NationalID": NationalID,
"NationalCode": NationalCode,
"BirthDate":BirthDate,
"Image": Image,
"isFemale": isFemale,
"Age": Age,
"Avg": Avg
]
Alamofire.request(BASE_URL, method: .post, parameters: body, encoding: JSONEncoding.default, headers: HEADER).responseJSON { (response) in
if response.response?.statusCode == 200 {
guard let data = response.result.value else { return }
print(data)
completion(true)
} else {
print("error reg auth service")
guard let er = response.result.value else { return }
print(er)
completion(false)
debugPrint(response.result.error as Any)
}
}
}
func get(FirstName: String, LastName: String, NationalID: String, NationalCode: String, BirthDate: Date,Image: String,isFemale: Bool,Age:Int64,Avg:Double, completion: #escaping CompletionHandler) -> [Person] {
let body: [String:Any] = [
"FirstName":FirstName,
"LastName": LastName,
"NationalID": NationalID,
"NationalCode": NationalCode,
"BirthDate":BirthDate,
"Image": Image,
"isFemale": isFemale,
"Age": Age,
"Avg": Avg
]
Alamofire.request(BASE_URL, method: .get, parameters: body, encoding: JSONEncoding.default, headers: HEADER).responseJSON { (response) in
if response.response?.statusCode == 200 {
print("no error login in authservice")
guard let data = response.result.value else { return }
print(data)
completion(true)
}
else if response.response?.statusCode == 400 {
print("error 400 login in authservice")
guard let er = response.result.value else { return }
print(er)
debugPrint(response.result.error as Any)
completion(false)
} else {
print("error ## login in authservice")
guard let er = response.result.value else { return }
print(er)
debugPrint(response.result.error as Any)
completion(false)
}
}
return [Person]()
}
}
The best idea is to use the URLRequestConvertible Alamofires protocol and create your own protocol and simple structs for every API request:
import Foundation
import Alamofire
protocol APIRouteable: URLRequestConvertible {
var baseURL: String { get }
var path: String { get }
var method: HTTPMethod { get }
var parameters: Parameters? { get }
var encoding: ParameterEncoding { get }
}
extension APIRouteable {
var baseURL: String { return URLs.baseURL }
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try baseURL.asURL().appendingPathComponent(path)
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
return try encoding.encode(urlRequest, with: parameters)
}
}
and request can look like this:
struct GetBooks: APIRouteable {
var path: String
var method: HTTPMethod
var parameters: Parameters?
var encoding: ParameterEncoding
}
and inside the APIClient prepare the generic method:
func perform<T: Decodable>(_ apiRoute: APIRouteable,
completion: #escaping (Result<T>) -> Void) {
let dataRequest = session
.request(apiRoute)
dataRequest
.validate(statusCode: 200..<300)
.responseDecodable(completionHandler: { [weak dataRequest] (response: DataResponse<T>) in
dataRequest.map { debugPrint($0) }
let responseData = response.data ?? Data()
let string = String(data: responseData, encoding: .utf8)
print("Repsonse string: \(string ?? "")")
switch response.result {
case .success(let response):
completion(.success(response))
case .failure(let error):
completion(.failure(error))
}
})
}
and use it:
func getBooks(completion: #escaping (Result<[Book]>) -> Void) {
let getBooksRoute = APIRoute.GetBooks()
perform(getBooksRoute, completion: completion)
}

unable to upload file RXSwift Moya multipart

I'm using Moya for handling communication between my swift application and api, I'm able to post and get data but unable to post file to api server, following is my code
enum TestApi {
...
case PostTest(obj: [String: AnyObject])
...
}
extension TestApi: TargetType {
var baseURL: NSURL {
switch self {
case .PostTest:
return NSURL(string: "http://192.168.9.121:3000")!
}
}
var path: String {
switch self {
case .PostTest:
return "/api/file"
}
}
var method: Moya.Method {
switch self {
case .PostTest:
return .POST
}
}
var parameters: [String: AnyObject]? {
switch self {
case .PostTest(let obj):
return ["image": obj["image"]!]
}
}
var sampleData: NSData {
return "".dataUsingEncoding(NSUTF8StringEncoding)!
}
var multipartBody: [MultipartFormData]? {
switch self {
case .PostTest(let multipartData):
guard let image = multipartData["image"] as? [NSData] else { return[] }
let formData: [MultipartFormData] = image.map{MultipartFormData(provider: .Data($0), name: "images", mimeType: "image/jpeg", fileName: "photo.jpg")}
return formData
default:
return []
}
}
}
and following is the way I called
return testApiProvider.request(.PostTest(obj: _file)).debug().mapJSON().map({ JSON -> EKResponse? in
return Mapper<EKResponse>().map(JSON)
})
I dont receive no response and no hit was sent to the api server
Multipart body is deprecated in Moya 8.0.0. Instead of that use Task for uploading.
Check this issue:
Moya multipart upload target
the subscription is missing in the calling code. This is not really a Moya problem, but problem with Reactive Extensions. the following .subscribeNext { _ in } fixed my issue
return testApiProvider
.request(.PostTest(obj: _file))
.debug()
.mapJSON()
.map({ JSON -> EKResponse? in
return Mapper<EKResponse>().map(JSON)
})
.subscribeNext { _ in }

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)