Search code examples
iosswiftalamofire

iOS create generic Alamofire request using swift


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 :)


Solution

  • 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:

    1. data: [String,Any] = In this you will put your form data.

    2. headers: [String:String] = In this you can send custom headers that you want to send along with the request

    3. 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

    4. 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.

    5. method: HTTPMethod = here you can specify which HTTP Method the request should use.

    6. 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")
    
    1. 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}
    
    1. with custom headers

      NetworkCall(data: ["username":"[email protected]"], 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": "[email protected]"]
    {
      "args": {}, 
      "data": "{\"username\":\"[email protected]\"}", 
      "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": "[email protected]"
      }, 
      "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.