There is a function getUser in RequestManager class that called in my VC.
func getUser(onCompletion: #escaping (_ result: User?, error: String?) -> Void) {
Alamofire.request(Router.getUser).responseJSON { (response) in
// here is the work with response
}
}
If this request returns 403 it means access_token is expired. I need to refresh token and repeat the request from my VC.
Now the question.
How to refresh token and repeat the request in the right way?
To handle the error and refresh token in MyViewController or getUser method is not good idea because I have a lot of VCs and request methods.
I need something like: VC calls the method and gets the User even if token is expired and refreshToken must not be in all request methods.
EDIT
refreshToken method
func refreshToken(onCompletion: #escaping (_ result: Bool?) -> Void) {
Alamofire.request(Router.refreshToken).responseJSON { (response) in
print(response)
if response.response?.statusCode == 200 {
guard let data = response.data else { return onCompletion(false) }
let token = try? JSONDecoder().decode(Token.self, from: data)
token?.setToken()
onCompletion(true)
} else {
onCompletion(false)
}
}
}
To solve this, I created a class from which we will call every API, say BaseService.swift.
BaseService.swift :
import Foundation
import Alamofire
import iComponents
struct AlamofireRequestModal {
var method: Alamofire.HTTPMethod
var path: String
var parameters: [String: AnyObject]?
var encoding: ParameterEncoding
var headers: [String: String]?
init() {
method = .get
path = ""
parameters = nil
encoding = JSONEncoding() as ParameterEncoding
headers = ["Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Cache-Control": "no-cache"]
}
}
class BaseService: NSObject {
func callWebServiceAlamofire(_ alamoReq: AlamofireRequestModal, success: #escaping ((_ responseObject: AnyObject?) -> Void), failure: #escaping ((_ error: NSError?) -> Void)) {
// Create alamofire request
// "alamoReq" is overridden in services, which will create a request here
let req = Alamofire.request(alamoReq.path, method: alamoReq.method, parameters: alamoReq.parameters, encoding: alamoReq.encoding, headers: alamoReq.headers)
// Call response handler method of alamofire
req.validate(statusCode: 200..<600).responseJSON(completionHandler: { response in
let statusCode = response.response?.statusCode
switch response.result {
case .success(let data):
if statusCode == 200 {
Logs.DLog(object: "\n Success: \(response)")
success(data as AnyObject?)
} else if statusCode == 403 {
// Access token expire
self.requestForGetNewAccessToken(alaomReq: alamoReq, success: success, failure: failure)
} else {
let errorDict: [String: Any] = ((data as? NSDictionary)! as? [String: Any])!
Logs.DLog(object: "\n \(errorDict)")
failure(errorTemp as NSError?)
}
case .failure(let error):
Logs.DLog(object: "\n Failure: \(error.localizedDescription)")
failure(error as NSError?)
}
})
}
}
extension BaseService {
func getAccessToken() -> String {
if let accessToken = UserDefaults.standard.value(forKey: UserDefault.userAccessToken) as? String {
return "Bearer " + accessToken
} else {
return ""
}
}
// MARK: - API CALL
func requestForGetNewAccessToken(alaomReq: AlamofireRequestModal, success: #escaping ((_ responseObject: AnyObject?) -> Void), failure: #escaping ((_ error: NSError?) -> Void) ) {
UserModal().getAccessToken(success: { (responseObj) in
if let accessToken = responseObj?.value(forKey: "accessToken") {
UserDefaults.standard.set(accessToken, forKey: UserDefault.userAccessToken)
}
// override existing alaomReq (updating token in header)
var request: AlamofireRequestModal = alaomReq
request.headers = ["Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Cache-Control": "no-cache",
"X-Authorization": self.getAccessToken()]
self.callWebServiceAlamofire(request, success: success, failure: failure)
}, failure: { (_) in
self.requestForGetNewAccessToken(alaomReq: alaomReq, success: success, failure: failure)
})
}
}
For calling the API from this call, we need to create a object of AlamofireRequestModal and override it with necessary parameter.
For example I created a file APIService.swift in which we have a method for getUserProfileData.
APIService.swift :
import Foundation
let GET_USER_PROFILE_METHOD = "user/profile"
struct BaseURL {
// Local Server
static let urlString: String = "http://192.168.10.236: 8084/"
// QAT Server
// static let urlString: String = "http://192.171.286.74: 8080/"
static let staging: String = BaseURL.urlString + "api/v1/"
}
class APIService: BaseService {
func getUserProfile(success: #escaping ((_ responseObject: AnyObject?) -> Void), failure: #escaping ((_ error: NSError?) -> Void)) {
var request: AlamofireRequestModal = AlamofireRequestModal()
request.method = .get
request.path = BaseURL.staging + GET_USER_PROFILE_METHOD
request.headers = ["Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Cache-Control": "no-cache",
"X-Authorization": getAccessToken()]
self.callWebServiceAlamofire(request, success: success, failure: failure)
}
}
Explanation:
In code block:
else if statusCode == 403 {
// Access token expire
self.requestForGetNewAccessToken(alaomReq: alamoReq, success: success, failure: failure)
}
I call getNewAccessToken API (say refresh-token, in your case), with the request( it could be any request based from APIService.swift).
When we get new token I save it user-defaults then I will update the request( the one I am getting as a parameter in refresh-token API call), and will pass the success and failure block as it is.
You can create generic refresher class:
protocol IRefresher {
associatedtype RefreshTarget: IRefreshing
var target: RefreshTarget? { get }
func launch(repeats: Bool, timeInterval: TimeInterval)
func invalidate()
}
class Refresher<T: IRefreshing>: IRefresher {
internal weak var target: T?
private var timer: Timer?
init(target: T?) {
self.target = target
}
public func launch(repeats: Bool, timeInterval: TimeInterval) {
timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: repeats) { [weak self] (timer) in
self?.target?.refresh()
}
}
public func invalidate() {
timer?.invalidate()
}
}
And the refresh target protocol:
protocol IRefreshing: class {
func refresh()
}
Define new typealias:
typealias RequestManagerRefresher = Refresher<RequestManager>
Now create refresher and store it:
class RequestManager {
let refresher: RequestManagerRefresher
init() {
refresher = Refresher(target: self)
refresher?.launch(repeats: true, timeInterval: 15*60)
}
}
And expand RequestManager:
extension RequestManager: IRefreshing {
func refresh() {
updateToken()
}
}
Every 15 minutes your RequestManager's token will be updated
UPDATE
Of course, you also can change the update time. Create a static var that storing update time you need. For example inside the RequestManager:
class RequestManager {
static var updateInterval: TimeInterval = 0
let refresher: RequestManagerRefresher
init() {
refresher = Refresher(target: self)
refresher?.launch(repeats: true, timeInterval: updateInterval)
}
}
So now you can ask the token provider server for token update interval and set this value to updateInterval static var:
backendTokenUpdateIntervalRequest() { interval in
RequestManager.updateInterval = interval
}
You can easily Refresh token and retry your previous API call using Alamofire
RequestInterceptor
NetworkManager.Swift:-
import Alamofire
class NetworkManager {
static let shared: NetworkManager = {
return NetworkManager()
}()
typealias completionHandler = ((Result<Data, CustomError>) -> Void)
var request: Alamofire.Request?
let retryLimit = 3
func request(_ url: String, method: HTTPMethod = .get, parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.queryString, headers: HTTPHeaders? = nil,
interceptor: RequestInterceptor? = nil, completion: #escaping completionHandler) {
AF.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers, interceptor: interceptor ?? self).validate().responseJSON { (response) in
if let data = response.data {
completion(.success(data))
} else {
completion(.failure())
}
}
}
}
RequestInterceptor.swift :-
import Alamofire
extension NetworkManager: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var request = urlRequest
guard let token = UserDefaultsManager.shared.getToken() else {
completion(.success(urlRequest))
return
}
let bearerToken = "Bearer \(token)"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
print("\nadapted; token added to the header field is: \(bearerToken)\n")
completion(.success(request))
}
func retry(_ request: Request, for session: Session, dueTo error: Error,
completion: #escaping (RetryResult) -> Void) {
guard let statusCode = request.response?.statusCode else {
completion(.doNotRetry)
return
}
guard request.retryCount < retryLimit else {
completion(.doNotRetry)
return
}
print("retry statusCode....\(statusCode)")
switch statusCode {
case 200...299:
completion(.doNotRetry)
case 401:
refreshToken { isSuccess in isSuccess ? completion(.retry) : completion(.doNotRetry) }
break
default:
completion(.retry)
}
}
func refreshToken(completion: #escaping (_ isSuccess: Bool) -> Void) {
let params = [
"refresh_token": Helpers.getStringValueForKey(Constants.REFRESH_TOKEN)
]
AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default).responseJSON { response in
if let data = response.data, let token = (try? JSONSerialization.jsonObject(with: data, options: [])
as? [String: Any])?["access_token"] as? String {
UserDefaultsManager.shared.setToken(token: token)
print("\nRefresh token completed successfully. New token is: \(token)\n")
completion(true)
} else {
completion(false)
}
}
}
}
Alamofire v5 has a property named RequestInterceptor.
RequestInterceptor has two method, one is Adapt which assign
access_token to any Network call header, second one is Retry method.
In Retry method we can check response status code and call
refresh_token block to get new token and retry previous API again.
Related
So, I have this function which is copied from this GitHub gist.
protocol ClientProtocol {
func request<Response: Codable>(_ endpoint: Endpoint<Response>)-> Single<Response>
}
final class Client: ClientProtocol {
private let manager: Alamofire.Session
private let baseURL = URL(string: "http://192.168.20:8080")!
private let queue = DispatchQueue(label: "<your_queue_label>")
init(accessToken: String) {
let configuration = URLSessionConfiguration.default
configuration.headers.add(name: "Authorization", value: "Bearer \(accessToken)")
configuration.timeoutIntervalForRequest = 15
self.manager = Alamofire.Session.init(configuration: configuration)
//self.manager.retrier = OAuth2Retrier()
}
func request<Response>(_ endpoint: Endpoint<Response>) -> Single<Response> {
return Single<Response>.create { observer in
let request = self.manager.request(
self.url(path: endpoint.path),
method: httpMethod(from: endpoint.method),
parameters: endpoint.parameters
)
request
.validate()
.responseData(queue: self.queue) { response in
let result: Result<Response, AFError> = response.result.flatMap(endpoint.decode)
switch result {
case let .success(val): observer(.success(val))
case let .failure(err): observer(.error(err))
}
}
return Disposables.create {
request.cancel()
}
}
}
private func url(path: Path) -> URL {
return baseURL.appendingPathComponent(path)
}
}
private func httpMethod(from method: Method) -> Alamofire.HTTPMethod {
switch method {
case .get: return .get
case .post: return .post
case .put: return .put
case .patch: return .patch
case .delete: return .delete
}
}
private class OAuth2Retrier: Alamofire.RequestRetrier {
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
if (error as? AFError)?.responseCode == 401 {
// TODO: implement your Auth2 refresh flow
// See https://github.com/Alamofire/Alamofire#adapting-and-retrying-requests
}
// completion(false, 0)
}
}
Endpoint
// MARK: Defines
typealias Parameters = [String: Any]
typealias Path = String
enum Method {
case get, post, put, patch, delete
}
// MARK: Endpoint
final class Endpoint<Response> {
let method: Method
let path: Path
let parameters: Parameters?
let decode: (Data) throws -> Response
init(method: Method = .get, path: Path, parameters: Parameters? = nil, decode: #escaping (Data) throws -> Response) {
self.method = method
self.path = path
self.parameters = parameters
self.decode = decode
}
}
// MARK: Convenience
extension Endpoint where Response: Swift.Decodable {
convenience init(method: Method = .get, path: Path, parameters: Parameters? = nil) {
self.init(method: method, path: path, parameters: parameters) {
try JSONDecoder().decode(Response.self, from: $0)
}
}
}
extension Endpoint where Response == Void {
convenience init(method: Method = .get, path: Path, parameters: Parameters? = nil) {
self.init(method: method, path: path, parameters: parameters, decode: { _ in () })
}
}
At let result = response.result.flatMap(endpoint.decode) xCode is throwing
Type of expression is ambiguous without more context
The type of the response.result is Result<Data, AFError>
I want to flatmap it to Result< Response, AFError>.
I tried
let result = response.result.flatMap { (data) -> Result<Response, AFError> in
// don't know what to put here
}
flatMap(_:) doesn't take throwing closure and in EndPoint, decode is a throwing closure :
let decode: (Data) throws -> Response
try catching the error:
func request<Response>(_ endpoint: Endpoint<Response>) -> Single<Response> {
return Single<Response>.create { observer in
let request = self.manager.request(
self.url(path: endpoint.path),
method: httpMethod(from: endpoint.method),
parameters: endpoint.parameters
)
request
.validate()
.responseData(queue: self.queue) { response in
let result: Result<Response, AFError> = response.result.flatMap {
do {
return Result<Response, AFError>.success(try endpoint.decode($0))
} catch let error as AFError {
return Result<Response, AFError>.failure(error)
} catch {
fatalError(error.localizedDescription)
}
}
switch result {
case let .success(val): observer(.success(val))
case let .failure(err): observer(.failure(err))
}
}
return Disposables.create {
request.cancel()
}
}
}
Note
Since you seem only interested in AFError this code will call fatalError this may not a good idea though.
You are using flatMap when you should be using map...
func example(response: Response, endpoint: Endpoint<Thing>) {
let result = response.result.map(endpoint.decode)
}
struct Response {
let result: Result<Data, Error>
}
struct Endpoint<T> {
func decode(_ data: Data) -> T { fatalError() }
}
struct Thing { }
Recently I have started learning iOS app development using swift so I am new to it. I want to implement rest api call in swift & found that we can achieve this using URLRequest. So I have written generic method to call all type(like get, put, post) of rest api as below.
import Foundation
//import Alamofire
public typealias JSON = [String: Any]
public typealias HTTPHeaders = [String: String];
public enum RequestMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
public enum Result<Value> {
case success(Value)
case failure(Error)
}
public class apiClient{
private var base_url:String = "https://api.testserver.com/"
private func apiRequest(endPoint: String,
method: RequestMethod,
body: JSON? = nil,
token: String? = nil,
completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) {
let url = URL(string: (base_url.self + endPoint))!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
urlRequest.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
if let token = token {
urlRequest.setValue("bearer " + token, forHTTPHeaderField: "Authorization")
}
if let body = body {
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body)
}
let session = URLSession(configuration: .default)
let task = session.dataTask(with: urlRequest) { data, response, error in
//NSLog(error)
completionHandler(data, response, error)
}
task.resume()
}
public func sendRequest<T: Decodable>(for: T.Type = T.self,
endPoint: String,
method: RequestMethod,
body: JSON? = nil,
token: String? = nil,
completion: #escaping (Result<T>) -> Void) {
return apiRequest(endPoint: endPoint, method: method, body:body, token: token) { data, response, error in
guard let data = data else {
return completion(.failure(error ?? NSError(domain: "SomeDomain", code: -1, userInfo: nil)))
}
do {
let decoder = JSONDecoder()
try completion(.success(decoder.decode(T.self, from: data)))
} catch let decodingError {
completion(.failure(decodingError))
}
}
}
}
this is how I call it method from controller
public func getProfile(userId :Int, objToken:String) -> Void {
let objApi = apiClient()
objApi.sendRequest(for: ProfileDetails.self,
endPoint:"api/user/profile/\(userId)",
method: .get,
token: objToken,
completion:
{(userResult: Result<ProfileDetails>) -> Void in
switch userResult
{
case .success(let value):
if value.respCode == "01" {
print(value.profile)
do {
//... ddo some taks like store response in local db or else
} catch let error as NSError {
// handle error
print(error)
}
}
else {
//do some task
}
break
case .failure(let error):
print(error)
break
}
})
}
I am decoding server response in below model
class ProfileDetails : Response, Decodable {
var appUpdate : AppUpdate?
var profile : Profile?
enum CodingKeys: String, CodingKey {
case profile = "profile"
case respCode = "resp_code"
case respMsg = "resp_msg"
}
public required convenience init(from decoder: Decoder) throws {
self.init()
let values = try decoder.container(keyedBy: CodingKeys.self)
self.profile = try values.decodeIfPresent(Profile.self, forKey: .profile)
self.respCode = try values.decodeIfPresent(String.self, forKey: .respCode)!
self.respMsg = try values.decodeIfPresent(String.self, forKey: .respMsg)
}
}
This code is not able to handle error response like 401, 404 etc from server. So what I am looking for, is to convert this api (URLRequest)request to generic Alamofire request with error handling like 401, 404 etc. I have install Alamofire pods. Is there anyone who has developed generic Alamofire request method with decoding & error handling?
Thanks in advance :)
Git link: https://github.com/sahilmanchanda2/wrapper-class-for-alamofire
Here is my version(Using Alamofire 5.0.2):
import Foundation
import Alamofire
class NetworkCall : NSObject{
enum services :String{
case posts = "posts"
}
var parameters = Parameters()
var headers = HTTPHeaders()
var method: HTTPMethod!
var url :String! = "https://jsonplaceholder.typicode.com/"
var encoding: ParameterEncoding! = JSONEncoding.default
init(data: [String:Any],headers: [String:String] = [:],url :String?,service :services? = nil, method: HTTPMethod = .post, isJSONRequest: Bool = true){
super.init()
data.forEach{parameters.updateValue($0.value, forKey: $0.key)}
headers.forEach({self.headers.add(name: $0.key, value: $0.value)})
if url == nil, service != nil{
self.url += service!.rawValue
}else{
self.url = url
}
if !isJSONRequest{
encoding = URLEncoding.default
}
self.method = method
print("Service: \(service?.rawValue ?? self.url ?? "") \n data: \(parameters)")
}
func executeQuery<T>(completion: #escaping (Result<T, Error>) -> Void) where T: Codable {
AF.request(url,method: method,parameters: parameters,encoding: encoding, headers: headers).responseData(completionHandler: {response in
switch response.result{
case .success(let res):
if let code = response.response?.statusCode{
switch code {
case 200...299:
do {
completion(.success(try JSONDecoder().decode(T.self, from: res)))
} catch let error {
print(String(data: res, encoding: .utf8) ?? "nothing received")
completion(.failure(error))
}
default:
let error = NSError(domain: response.debugDescription, code: code, userInfo: response.response?.allHeaderFields as? [String: Any])
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
})
}
}
The above class uses latest Alamofire version (as of now Feb 2020), This class covers almost every HTTP Method with option to send data in Application/JSON format or normal. With this class you get a lot of flexibility and it automatically converts response to your Swift Object.
Look at the init method of this class it has:
data: [String,Any] = In this you will put your form data.
headers: [String:String] = In this you can send custom headers that you want to send along with the request
url = Here you can specify full url, you can leave it blank if you already have defined baseurl in Class. it comes handy when you want to consume a REST service provided by a third party. Note: if you are filling the url then you should the next parameter service should be nil
service: services = It's an enum defined in the NetworkClass itself. these serves as endPoints. Look in the init method, if the url is nil but the service is not nil then it will append at the end of base url to make a full URL, example will be provided.
method: HTTPMethod = here you can specify which HTTP Method the request should use.
isJSONRequest = set to true by default. if you want to send normal request set it to false.
In the init method you can also specify common data or headers that you want to send with every request e.g. your application version number, iOS Version etc
Now Look at the execute method: it's a generic function which will return swift object of your choice if the response is success. It will print the response in string in case it fails to convert response to your swift object. if the response code doesn't fall under range 200-299 then it will be a failure and give you full debug description for detailed information.
Usage:
say we have following struct:
struct Post: Codable{
let userId: Int
let id: Int
let title: String
let body: String
}
Note the base url defined in NetworkClass https://jsonplaceholder.typicode.com/
Example 1: Sending HTTP Post with content type Application/JSON
let body: [String : Any] = ["title": "foo",
"body": "bar",
"userId": 1]
NetworkCall(data: body, url: nil, service: .posts, method: .post).executeQuery(){
(result: Result<Post,Error>) in
switch result{
case .success(let post):
print(post)
case .failure(let error):
print(error)
}
}
output:
Service: posts
data: ["userId": 1, "body": "bar", "title": "foo"]
Post(userId: 1, id: 101, title: "foo", body: "bar")
HTTP 400 Request
NetworkCall(data: ["email":"peter#klaven"], url: "https://reqres.in/api/login", method: .post, isJSONRequest: false).executeQuery(){
(result: Result) in
switch result{
case .success(let post):
print(post)
case .failure(let error):
print(error)
}
}
output:
Service: https://reqres.in/api/login
data: ["email": "peter#klaven"]
Error Domain=[Request]: POST https://reqres.in/api/login
[Request Body]:
email=peter%40klaven
[Response]:
[Status Code]: 400
[Headers]:
Access-Control-Allow-Origin: *
Content-Length: 28
Content-Type: application/json; charset=utf-8
Date: Fri, 28 Feb 2020 05:41:26 GMT
Etag: W/"1c-NmpazMScs9tOqR7eDEesn+pqC9Q"
Server: cloudflare
Via: 1.1 vegur
cf-cache-status: DYNAMIC
cf-ray: 56c011c8ded2bb9a-LHR
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
x-powered-by: Express
[Response Body]:
{"error":"Missing password"}
[Data]: 28 bytes
[Network Duration]: 2.2678009271621704s
[Serialization Duration]: 9.298324584960938e-05s
[Result]: success(28 bytes) Code=400 "(null)" UserInfo={cf-ray=56c011c8ded2bb9a-LHR, Access-Control-Allow-Origin=*, Date=Fri, 28 Feb 2020 05:41:26 GMT, expect-ct=max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct", Server=cloudflare, Etag=W/"1c-NmpazMScs9tOqR7eDEesn+pqC9Q", x-powered-by=Express, Content-Type=application/json; charset=utf-8, Content-Length=28, Via=1.1 vegur, cf-cache-status=DYNAMIC}
with custom headers
NetworkCall(data: ["username":"sahil.manchanda2#gmail.com"], headers: ["custom-header-key" : "custom-header-value"], url: "https://httpbin.org/post", method: .post).executeQuery(){(result: Result) in
switch result{
case .success(let data):
print(data)
case .failure(let error):
print(error)
}
}
output:
Service: https://httpbin.org/post
data: ["username": "sahil.manchanda2#gmail.com"]
{
"args": {},
"data": "{\"username\":\"sahil.manchanda2#gmail.com\"}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8",
"Accept-Language": "en;q=1.0",
"Content-Length": "41",
"Content-Type": "application/json",
"Custom-Header-Key": "custom-header-value",
"Host": "httpbin.org",
"User-Agent": "NetworkCall/1.0 (sahil.NetworkCall; build:1; iOS 13.2.2) Alamofire/5.0.2",
"X-Amzn-Trace-Id": "Root=1-5e58a94f-fab2f24472d063f4991e2cb8"
},
"json": {
"username": "sahil.manchanda2#gmail.com"
},
"origin": "182.77.56.154",
"url": "https://httpbin.org/post"
}
typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode String but found a dictionary instead.", underlyingError: nil))
In the last example you can see typeMismatch at the end, I tried to pass [String:Any] in the executeQuery but since the Any doesn't confirm to encodable I had to use String.
I use EVReflection with alamofire and i think this is one of the best combination to work with.
Use URLRequestConvertible protocol of Alamofire.
This is what i follow.
Just for reference purpose.
Make enum for your all endpoint and confirm that enum to URLRequestConvertible.
enum Router: URLRequestConvertible {
//your all endpoint
static var authToken = ""
case login([String:Any])
var route: Route {
switch self {
case .Login(let dict):
return Route(endPoint: "api/addimagedata", httpMethod: .post)
}
}
func asURLRequest() throws -> URLRequest {
var requestUrl = EnvironmentVariables.baseURL
if let queryparams = route.queryParameters {
requestUrl.appendQueryParameters(queryparams)
}
var mutableURLRequest = URLRequest(url: requestUrl.appendingPathComponent(route.endPath))
mutableURLRequest.httpMethod = route.method.rawValue
//FIXME:- Change the Userdefault Key
if Router.authToken.isEmpty, let token = UserDefaults.standard.string(forKey: "Key"), !token.isEmpty {
Router.authToken = token
}
//FIXME:- Set Mutable Request Accordingly
mutableURLRequest.setValue("Bearer \(Router.authToken)", forHTTPHeaderField: "Authorization")
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Accept")
if route.method == .get {
return try Alamofire.URLEncoding.default.encode(mutableURLRequest, with: route.parameters)
}
return try Alamofire.JSONEncoding.default.encode(mutableURLRequest, with: route.parameters)
}
}
Make One Structure as per your requirement.
struct Route {
let endPath: String
let method: Alamofire.HTTPMethod
var parameters: Parameters?
var queryParameters : [String:String]?
var encoding: Alamofire.ParameterEncoding {
switch method {
case .post, .put, .patch, .delete:
return JSONEncoding()
default:
return URLEncoding()
}
}
}
Now make one generic function that accept URLRequestConvertible and return your model in closure. Something like this.
func GenericApiCallForObject<T : URLRequestConvertible, M : EVObject>(router : T, showHud : Bool = true ,responseModel : #escaping (M) -> ()) {
view.endEditing(true)
if !isConnectedToInternet {
showNetworkError()
return
}
if showhud ? showHud() : ()
Alamofire.request(router).responseObject { (response: DataResponse<M>) in
self.HandleResponseWithErrorForObject(response: response) { (isSuccess) in
if isSuccess {
if let value = response.result.value {
responseModel(value)
}
}
})
}
}
Now make one generic function that accept your response and handle the error for you. Something like this.
func HandleResponseWithErrorForObject<M : EVObject>(response : DataResponse<M>, isSuccess : #escaping (Bool) -> ()) {
print(response)
hideHud()
switch response.response?.statusCode ?? 0 {
case 200...299:
isSuccess(true)
case 401:
isSuccess(false)
showSessionTimeOutError()
case -1005,-1001,-1003:
break
default:
isSuccess(false)
// Parse your response and show error in some way.
}
}
Now Finally, how to use it right??! Indeed now its very simple just two lines of code and you are good to go.
GenericApiCallForObject(router: Router.Login(["xyz":"xyz"])) { (response : GeneralModel) in
print(response)
}
Please note that this will only work if you are getting object in response. If there is an array or string you have to make separate function for that and procedure for that is same as above. You will only get response if there is a success otherwise HandleResponseWithErrorForObject function will automatically handle it for you. Also, some variables might be missing in above explanation.
I'm sharing a specific part for error handling on my REST api.
It will decode inside the following block and probably you can use it for reference.
As you can see that's very simple getting a code and translate into an enumeration.
Alamofire allow that but it depends on your version of library.
Sometimes depends your REST api how handle errors internally, they can not throw a code for example if its Java backend, they can encapsulate the exceptions.
public enum RESTError: Error {
case BadRequest(String, [String]?)
case InternalError(String)
case UnAuthorized(String, [String]?)
case NotFound(String)
case Success
/// <#Description#>
///
/// - Parameters:
/// - code: <#code description#>
/// - message: <#message description#>
/// - globalErrors: <#globalErrors description#>
/// - Returns: <#return value description#>
public static func fromCode(code: Int, message: String, globalErrors: [String]? = nil) -> RESTError {
switch code {
case 400: return RESTError.BadRequest(message, globalErrors)
case 401: return RESTError.UnAuthorized(message, globalErrors)
case 500: return RESTError.InternalError(message)
case 404: return RESTError.NotFound(message)
default: break
}
return RESTError.Success
}
}
Alamofire.request(urlRequest)
.validate(statusCode: 200...500)
.responseJSON(completionHandler: { (response: (DataResponse<Any>)) in
if let statusCode = response.response?.statusCode {
if statusCode != 200 {
// call handler errors function with specific message
if let arrayDictionary = response.result.value as? Dictionary<String,AnyObject> {
var error: RESTError?
if let code = arrayDictionary["status"] as? Int {
let message = arrayDictionary["message"] as! String
let globalErrors = arrayDictionary["globalErrors"] as? [String]
error = RESTError.fromCode(code: code, message: message, globalErrors: globalErrors)
} else {
// Build from error message without code.
let message = arrayDictionary["error_description"] as! String
let codeMsg = arrayDictionary["error"] as! String
let globalErrors = arrayDictionary["globalErrors"] as? [String]
if codeMsg == "invalid_token" && message.starts(with: "Access token expired") {
return
} else {
error = RESTError.fromCode(code: codeMsg, message: message, globalErrors: globalErrors)
}
}
if let _ = error {
errorHandler(error!)
} else {
errorHandler(RESTError.InternalError("Internal API rest error."))
}
} else {
errorHandler(RESTError.fromCode(code: statusCode, message: ""))
}
} else {
if let arrayDictionary = response.result.value as? Dictionary<String,AnyObject> {
handler(arrayDictionary)
}
}
} else {
if let error = response.error {
errorHandler(RESTError.InternalError(error.localizedDescription))
}
}
})
You probably need this function that uses the alamofilre Session Manager to perform requests. You can also set the cookies ant headers etc.. to this session manager so that you will have them to the rest of your requests.
import Alamofire
class NetworkManager : NSObject {
internal typealias SuccessCompletion = (Int?, Any?) -> Void?
internal typealias FailCompletion = (Int?, Error, Any?) -> Void?
var sessionManager : SessionManager!
var request : Request?
var headers : HTTPHeaders! = [:]
override init() {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
sessionManager = SessionManager(configuration: configuration)
}
func sendRequest(url: String?, method: String, parameters: [String: Any], success: SuccessCompletion?, fail: FailCompletion?){
var encoding : ParameterEncoding!
if HTTPMethod(rawValue: method) == HTTPMethod.post {
encoding = JSONEncoding.default
} else {
encoding = URLEncoding.default
}
request = sessionManager.request(url ?? "", method: HTTPMethod(rawValue: method)!, parameters: parameters, encoding: encoding, headers: headers)
.validate()
.responseData{response in
switch (response.result) {
case .success:
let statusCode = response.response?.statusCode
success?(statusCode, response.result.value)
self.request = nil
break
case .failure(let error):
let statusCode = response.response?.statusCode
fail?(statusCode, error, response.data)
self.request = nil
break
}
}
}
}
EDIT
To add Headers you can just add a function like this..
func updateJSONHeader(token: String) {
self.clearHeaders()
headers["AuthorizationToken"] = "\(token)"
}
For cookie
func setCookie(_ cookie : HTTPCookie?){
if let cookie = cookie {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
Clear headers
func clearHeaders(){
headers = [:]
}
And keep in mind that it's a singleton class so whenever you change anything unless your server make some changes you still have your configuration, ex. the headers
The best way is create a custom validate method using DataRequest extension:
func customValidate() -> Self {
return self.validate { _, response, data -> Request.ValidationResult in
guard (400...599) ~= response.statusCode else { return .success(()) }
guard let data = data else { return .failure(MyAppGeneralError.generalResponseError) }
guard let errorResponse = try? JSONDecoder().decode(MyAppResponseError.self, from: data) else {
return .failure(MyAppGeneralError.generalResponseError)
}
if response.statusCode == 401 {
return .failure(MyAppGeneralError.unauthorizedAccessError(errorResponse))
}
return .failure(MyAppGeneralError.responseError(errorResponse))
}
}
With a client with a generic function where the generic is decodable using our custom validate.
class APIClient {
var session: Session
init(session: Session = Session.default) {
self.session = session
}
#discardableResult
func performRequest<T: Decodable>(request: URLRequestConvertible,
decoder: JSONDecoder = JSONDecoder(),
completion: #escaping (Result<T, AFError>) -> Void) -> DataRequest {
return AF.request(request).customValidate().responseDecodable(decoder: decoder, completionHandler: { (response: DataResponse<T, AFError>) in
completion(response.result)
})
}
func getProfile(userID: Int, _ completion: #escaping (Result<UserToken, AFError>) -> Void) {
performRequest(request: APIRouter.profile(userID: userID), completion: completion)
}
}
using a router a:
enum APIRouter: URLRequestConvertible {
case profile(userId :Int)
static let baseURLString = "https://myserver.com"
var method: HTTPMethod {
switch self {
case .profile:
return .get
}
}
var path: String {
switch self {
case .profile(let userID):
return "profile/\(userID)"
}
}
var body: Parameters {
return [:]
}
// MARK: URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try APIRouter.baseURLString.asURL()
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
urlRequest.httpMethod = method.rawValue
// Common Headers
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
// Encode body
urlRequest = try JSONEncoding.default.encode(urlRequest, with: body)
return urlRequest
}
}
import Foundation
import UIKit
import Alamofire
import SwiftyJSON
class AFWrapper: NSObject {
static let sharedInstance = AFWrapper()
//TODO :-
/* Handle Time out request alamofire */
func requestGETURL(_ strURL: String, success:#escaping (JSON) -> Void, failure:#escaping (Error) -> Void)
{
Alamofire.request(strURL).responseJSON { (responseObject) -> Void in
//print(responseObject)
if responseObject.result.isSuccess {
let resJson = JSON(responseObject.result.value!)
//let title = resJson["title"].string
//print(title!)
success(resJson)
}
if responseObject.result.isFailure {
let error : Error = responseObject.result.error!
failure(error)
}
}
}
func requestPOSTURL(_ strURL : String, params : [String : AnyObject]?, headers : [String : String]?, success:#escaping (JSON) -> Void, failure:#escaping (Error) -> Void){
Alamofire.request(strURL, method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseJSON { (responseObject) -> Void in
//print(responseObject)
if responseObject.result.isSuccess {
let resJson = JSON(responseObject.result.value!)
success(resJson)
}
if responseObject.result.isFailure {
let error : Error = responseObject.result.error!
failure(error)
}
}
}
}
This is something I have been working on! Not finished yet but could solve your issue. you can upgrade it to whatever you want.
typealias
typealias Closure<T> = (T)->()
typealias JSON = [String: Any]
Extension
extension JSONDecoder{
func decode<T : Decodable>(_ model : T.Type,
result : #escaping Closure<T>) ->Closure<Data>{
return { data in
if let value = try? self.decode(model.self, from: data){
result(value)
}
}
}
Protocol
//MARK:- protocol APIResponseProtocol
protocol APIResponseProtocol{
func responseDecode<T: Decodable>(to modal : T.Type,
_ result : #escaping Closure<T>) -> APIResponseProtocol
func responseJSON(_ result : #escaping Closure<JSON>) -> APIResponseProtocol
func responseFailure(_ error :#escaping Closure<String>)
}
Request:
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 300 // seconds
configuration.timeoutIntervalForResource = 500
alamofireManager = Alamofire.SessionManager(configuration: configuration)
func getRequest(forAPI api: String, params: JSON) -> APIResponseProtocol {
let responseHandler = APIResponseHandler()
var parameters = params
parameters["token"] = preference.string(forKey: USER_ACCESS_TOKEN)
alamofireManager.request(api,
method: .get,
parameters: parameters,
encoding: URLEncoding.default,
headers: nil)
.responseJSON { (response) in
print("Å api : ",response.request?.url ?? ("\(api)\(params)"))
switch response.result{
case .success(let value):
let json = value as! JSON
let error = json.string("error")
guard error.isEmpty else{
responseHandler.handleSuccess(value: value,data: response.data ?? Data())
case .failure(let error):
responseHandler.handleFailure(value: error.localizedDescription)
}
}
return responseHandler
}
Response Hanlder:
class APIResponseHandler : APIResponseProtocol{
init(){
}
var jsonSeq : Closure<JSON>?
var dataSeq : Closure<Data>?
var errorSeq : Closure<String>?
func responseDecode<T>(to modal: T.Type, _ result: #escaping Closure<T>) -> APIResponseProtocol where T : Decodable {
let decoder = JSONDecoder()
self.dataSeq = decoder.decode(modal, result: result)
return self
}
func responseJSON(_ result: #escaping Closure<JSON>) -> APIResponseProtocol {
self.jsonSeq = result
return self
}
func responseFailure(_ error: #escaping Closure<String>) {
self.errorSeq = error
}
func handleSuccess(value : Any,data : Data){
if let jsonEscaping = self.jsonSeq{
jsonEscaping(value as! JSON)
}
if let dataEscaping = dataSeq{
dataEscaping(data)
}
}
func handleFailure(value : String){
self.errorSeq?(value)
}
}
USAGE:
self?.apiInteractor?
.getRequest(forAPI: "https://maps.googleapis.com/maps/api/directions/json",
params: [
"origin" : "\(pickUpLatitude),\(pickUpLongitude)",
"destination" :"\(dropLatitude),\(dropLongitude)",
"mode" : "driving",
"units" : "metric",
"sensor" : "true",
"key" : "\(UserDefaults.value(for: .google_api_key) ?? "")"
])
.responseDecode(to: GoogleGeocode.self, { [weak self] (googleGecode) in
guard let welf = self,
let route = googleGecode.routes.first,
let leg = route.legs.first else{return}
welf.tripDetailModel?.arrivalFromGoogle = leg.duration.text ?? ""
welf.drawRoute(forRoute: route)
welf.calculateETA()
})
.responseJSON({ (json) in
debugPrint(json.description)
})
.responseFailure({ (error) in
debug(print: error)
})
just part of code, but try
let req = Alamofire.request(url, method: .get, parameters: nil)
then you can handle response code by using
req.response?.statusCode
and handle response by for example
req.responseString(completionHandler: <#T##(DataResponse<String>) -> Void#>)
or
req.responseJSON(completionHandler: <#T##(DataResponse<Any>) -> Void#>)
you have good example here
Is there a way, in Alamofire, to re-send the request if the response code from the first request is 401, where I can refresh the token and retry my request again?
The problem is that I'm using MVVM and also completion handler already.
In my ViewModel the request function looks like:
public func getProfile(completion: #escaping (User?) -> Void) {
guard let token = UserDefaults.standard.value(forKey: Constants.shared.tokenKey) else { return }
let headers = ["Authorization": "Bearer \(token)"]
URLCache.shared.removeAllCachedResponses()
Alamofire.request(Constants.shared.getProfile, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON { (response) in
switch response.result {
case .success:
guard let data = response.data else { return }
if JSON(data)["code"].intValue == 401 {
// here I need to refresh my token and re-send the request
} else {
let user = User(json: JSON(data)["data"])
completion(user)
}
completion(nil)
case .failure(let error):
print("Failure, ", error.localizedDescription)
completion(nil)
}
}
}
and from my ViewController I call it like:
viewModel.getProfile { (user) in
if let user = user {
...
}
}
So I do not know how can retry my request without using a new function, so I can still get my user response from completion part in my ViewController.
Maybe someone can show me the right path.
Thanks in advance!
To retry a request create a Request wrapper and use the RequestInterceptor protocol of Alamofire like this
final class RequestInterceptorWrapper: RequestInterceptor {
// Retry your request by providing the retryLimit. Used to break the flow if we get repeated 401 error
var retryLimit = 0
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
guard let statusCode = request.response?.statusCode else { return }
switch statusCode {
case 200...299:
completion(.doNotRetry)
default:
if request.retryCount < retryLimit {
completion(.retry)
return
}
completion(.doNotRetry)
}
}
//This method is called on every API call, check if a request has to be modified optionally
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
//Add any extra headers here
//urlRequest.addValue(value: "", forHTTPHeaderField: "")
completion(.success(urlRequest))
}
}
Usage: For every API request, the adapt() method is called, and on validate() the retry method is used to validate the status code. retryLimit can be set by creating an instance of the interceptor here
Providing the retryLimit would call the API twice if the response was an error
let interceptor = RequestInterceptorWrapper()
func getDataFromAnyApi(completion: #escaping (User?) -> Void)) {
interceptor.retryLimit = 2
AF.request(router).validate().responseJSON { (response) in
guard let data = response.data else {
completion(nil)
return
}
// convert to User and return
completion(User)
}
}
Yes you can on Alamofire 4.0
The RequestRetrier protocol allows a Request that encountered an Error while being executed to be retried. When using both the RequestAdapter and RequestRetrier protocols together, you can create credential refresh systems for OAuth1, OAuth2, Basic Auth and even exponential backoff retry policies. The possibilities are endless. Here's an example of how you could implement a refresh flow for OAuth2 access tokens.
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let accessToken = accessToken, let refreshToken = refreshToken {
strongSelf.accessToken = accessToken
strongSelf.refreshToken = refreshToken
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
Reference: AlamofireDocumentation
you can add interceptor
Alamofire.request(Constants.shared.getProfile, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers)
add the protocol RequestInterceptor
then implement this two protocol method
// retryCount number of time api need to retry
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
completion(.success(urlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
guard request.retryCount < retryCount else {
completion(.doNotRetry)
return
}
/// Call UR API here
}
once api get fail this two method call, do
Could you just recursively call the function if it receives a 401? You would definitely need to create some type of exit condition so that if it continues to fail that it will break out, but it seems to me that it would work.
I am trying to implement my OAuth2 flow using Alamofire 5.0.0-beta.3. As i can see the documentation is still for Alamofire 4 as stated in the github page as well.
I am trying to make the Oauth2 handler following the documentation for Alamofire 4. As the class names are changed, I am completely lost while making it.
This is the code that i am following:
class OAuth2Handler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var clientID: String
private var baseURLString: String
private var accessToken: String
private var refreshToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
// MARK: - Initialization
public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
self.clientID = clientID
self.baseURLString = baseURLString
self.accessToken = accessToken
self.refreshToken = refreshToken
}
// MARK: - RequestAdapter
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
return urlRequest
}
return urlRequest
}
// MARK: - RequestRetrier
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let accessToken = accessToken, let refreshToken = refreshToken {
strongSelf.accessToken = accessToken
strongSelf.refreshToken = refreshToken
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
// MARK: - Private - Refresh Tokens
private func refreshTokens(completion: #escaping RefreshCompletion) {
guard !isRefreshing else { return }
isRefreshing = true
let urlString = "\(baseURLString)/oauth2/token"
let parameters: [String: Any] = [
"access_token": accessToken,
"refresh_token": refreshToken,
"client_id": clientID,
"grant_type": "refresh_token"
]
sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.responseJSON { [weak self] response in
guard let strongSelf = self else { return }
if
let json = response.result.value as? [String: Any],
let accessToken = json["access_token"] as? String,
let refreshToken = json["refresh_token"] as? String
{
completion(true, accessToken, refreshToken)
} else {
completion(false, nil, nil)
}
strongSelf.isRefreshing = false
}
}
}
This is how to use this for alamofire 4:
let baseURLString = "https://some.domain-behind-oauth2.com"
let oauthHandler = OAuth2Handler(
clientID: "12345678",
baseURLString: baseURLString,
accessToken: "abcd1234",
refreshToken: "ef56789a"
)
let sessionManager = SessionManager()
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandler
let urlString = "\(baseURLString)/some/endpoint"
sessionManager.request(urlString).validate().responseJSON { response in
debugPrint(response)
}
This is the link i am following to implement this.
https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests
Look at something like this.
struct EnvironmentInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest>) -> Void) {
var adaptedRequest = urlRequest
guard let token = AtraqService.shared.user?.token.accessToken else {
completion(.success(adaptedRequest))
return
}
adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
completion(.success(adaptedRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
//get token
}
}
}
Then
Session(configuration: configuration, interceptor: EnvironmentInterceptor())
Finally
request().validate().response...
If Alamofire 5 is not intercepting (adapting or retrying) your requests for some reason, then just try to check the delegate signature out as described here.
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var modifiedURLRequest = urlRequest
let apiToken = config.apiToken
modifiedURLRequest.setValue(apiToken, forHTTPHeaderField: Constants.apiTokenHeader)
completion(.success(modifiedURLRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
completion(.doNotRetry)
}
Here's the difference (look at the signature):
func adapt(_ urlRequest: URLRequest, for session: Alamofire.Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var modifiedURLRequest = urlRequest
let apiToken = config.apiToken
modifiedURLRequest.setValue(apiToken, forHTTPHeaderField: Constants.apiTokenHeader)
completion(.success(modifiedURLRequest))
}
func retry(_ request: Alamofire.Request, for session: Alamofire.Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
completion(.doNotRetry)
}
I was just wondering what would be the best way to call the same Web Service from Different View Controllers(at different time). What architecture or design should I follow? I don't want to write the same code in each View Controller.
In case of using Alamofire library I can suggest to use
class NetworkManager {
static let shared = NetworkManager()
static let alamofireManager: SessionManager = {
let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.timeoutIntervalForRequest = TimeInterval(_TIMEOUT)
sessionConfiguration.timeoutIntervalForResource = TimeInterval(_TIMEOUT)
return Alamofire.SessionManager(configuration: sessionConfiguration)
}()
func performRequest(url: String,
method: HTTPMethod = .get,
parameters: [String: Any] = [String: Any](),
encoding: ParameterEncoding = URLEncoding.default,
contentType: String? = nil,
headers: HTTPHeaders = [String: String](),
success: #escaping(Data, Int) -> (),
failure: #escaping(CustomError) -> ()) {
debugPrint("NetworkManager is calling endpoint: \(url)")
NetworkManager.alamofireManager.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers).validate().response { response in
guard let status = response.response?.statusCode, let data = response.data else {
if let error = response.error {
debugPrint("Error when calling endpoint \(url)")
failure(.unknownError(message: error.localizedDescription))
}
return
}
debugPrint("HTTP Status received: \(status)")
success(data, status)
}
} else {
failure(.noNetworkConnection)
}
}
Please feel free to modify failure handler with your custom error or whatever you like.
Of course then you need to serialise the response.
Create a base class for Alamofire request like that:
import Alamofire
/// Struct for create AlamofireRequestModal
struct AlamofireRequestModal {
/// Struct constant for Alamofire.HTTPMethod
var method: Alamofire.HTTPMethod
/// Struct constant for path
var path: String
/// Struct constant for parameters
var parameters: [String: AnyObject]?
/// Struct constant for encoding:ParameterEncoding
var encoding: ParameterEncoding
/// Struct constant for headers
var headers: [String: String]?
///method to get init
init() {
method = .post
path = ""
parameters = nil
encoding = JSONEncoding() as ParameterEncoding
}
}
///BaseService to call the api's
class BaseService: NSObject {
/// network variable for Reachability
let network = Reachability.init(hostname: "https://www.google.com")
/**
This is method for call WebService into Alamofire
- parameter alamoReq: this is AlamofireRequestModal type request
- parameter success: success response
- parameter failure: failer object
*/
func callWebServiceAlamofire(_ alamoReq: AlamofireRequestModal, success:#escaping ((_ responseObject: AnyObject?) -> Void), failure:#escaping ((_ error: NSError?) -> Void)) {
guard (network?.isReachable)! else {
debugPrint("\n No Network Connection")
return
}
let request = Alamofire.request(alamoReq.path, method: alamoReq.method, parameters: alamoReq.parameters, encoding: alamoReq.encoding, headers: alamoReq.headers)
// Call response handler method of alamofire
request.validate(statusCode: 200..<600).responseJSON(completionHandler: { response in
let statusCode = response.response?.statusCode
if let allHeaderField = response.response {
allHeaderField.setHeaders()
}
switch response.result {
case .success(let data):
if statusCode == 200 {
success(data as AnyObject)
} else {
failure(NSError.init(domain: "www.wen.com", code: 101010, userInfo: ["message": "Something went wrong. Please trt again."]))
}
case .failure(let error):
failure(error as NSError?)
}
})
}
}
Then create a service class according to use, like here I am creating Profile service class for login and registration and profile type all api's are added in this class so you can create multiple serice class according to use:
import Alamofire
///Profile service to call the profile api's
class ProfileService: BaseService {
/**
This is request to BaseService to get Login
- parameter email: User email id
- parameter password: User password to login
- parameter success: success response
- parameter failure: failure response
*/
func doLogin(email: String, password: String, success: #escaping ((_ response: AnyObject?) -> Void), failure: #escaping ((_ error: NSError?) -> Void)) {
var request = AlamofireRequestModal()
request.path = "www.yourpath.com"
request.parameters = ["email": email as AnyObject,
"password": password as AnyObject
]
callWebServiceAlamofire(request, success: success, failure: failure)
}
}
Now, You can call this doLogin method of Profile service from anywhere like Or you can create more layer like Model class and call this service from model class or You can call directly like this:
ProfileService().doLogin(email: "Email", password: "Password", success: { (response) in
// Code here for handle success response
}) { (error) in
// Code here for handle error
}
I would write a network manager class, that takes the web service parameters if any as arguments.
Here is a crude example of the architecture
class YourNetworkManager {
public func callSpecificWebService(argument : Type?, [optional closure to handle results]) {
// Generate the actual URL to be called here. Usually by
// appending a suffix to some constant base url
// The web service call mechanism goes here.
// This could either use the NSURLSession API
// or some third party library such as Alamofire
// Process the generic response conditions from the web service
// here. Pass on the specific parts to the calling method.
}
}
As I mentioned, this is a crude example. The more modularised you can make things, the better it will be.
Never pass your Views and/or ViewControllers to you NetworkManager class.
Suppose you have a NetworkManager class like this.
open class NetworkHelper {
class var sharedManager: NetworkHelper {
struct Static{
static let instance: NetworkHelper = NetworkHelper()
}
return Static.instance
}
func request(_ method: HTTPMethod, _ URLString: String, parameters: [String : AnyObject]? = [:], headers: [String : String]? = [:], completion:#escaping (Any?) -> Void, failure: #escaping (Error?) -> Void) {
let URL = "BASE_PATH" + "URLString"
Alamofire.request(URL, method: method, parameters: parameters, encoding: JSONEncoding.default, headers: headers)
.responseJSON { response in
switch response.result {
case .success:
completion(response.result.value!)
case .failure(let error):
failure(error)
guard error.localizedDescription == JSON_COULDNOT_SERIALISED else {
return
}
}
}
}
}
Now create a BaseViewController which inherit from UIViewController and write your API call with necessary parameters.
For example in an API call you only need userID as param all else is static.
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
func makeThisApiCallWithUserID(userId: Int) {
NetworkHelper.sharedManager.request(.get, "api/v1/yourApiAddress", parameters: ["userId":userId as AnyObject],headers: ["Authorization":"Bearer agshd81tebsf8724j"], completion: { (response) in
}) { (error) in
}
}
}
No you should inherit those ViewController in which you want the same API call and you do not want to write the code again.
class FirstChildViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.makeThisApiCallWithUserID(userId: 123)
}
}
class SecondChildViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.makeThisApiCallWithUserID(userId: 13)
}
}
class ThirdChildViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.makeThisApiCallWithUserID(userId: 3)
}
}
See I haven't write API code in FirstChildViewController, SecondChildViewController, ThirdChildViewController but still they can make the same API call with different parameters.
Do you use Alamofire?, if yes then I have good method for it, written in NetworkHelper Class.
import Foundation
import Alamofire
open class NetworkHelper {
class var sharedManager: NetworkHelper {
struct Static{
static let instance: NetworkHelper = NetworkHelper()
}
return Static.instance
}
func request(_ method: HTTPMethod
, _ URLString: String
, parameters: [String : AnyObject]? = [:]
, headers: [String : String]? = [:]
, onView: UIView?, vc: UIViewController, completion:#escaping (Any?) -> Void
, failure: #escaping (Error?) -> Void) {
let URL = BASE_PATH + URLString
Alamofire.request(URL, method: method, parameters: parameters, encoding: JSONEncoding.default, headers: headers)
.responseJSON { response in
switch response.result {
case .success:
completion(response.result.value!)
case .failure(let error):
failure(error)
guard error.localizedDescription == JSON_COULDNOT_SERIALISED else {
return
}
}
}
}
}