iOS 使用 swift 创建通用 Alamofire 请求

Aja*_*jay 5 ios swift alamofire

最近我开始学习使用 swift 开发 iOS 应用程序,所以我对它很陌生。我想在 swift 中实现 Rest api 调用,发现我们可以使用URLRequest. 所以我写了generic method调用所有类型(如get, put, post)的rest api,如下所示。

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))
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这就是我称之为方法的方式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
                }
        })
    }
Run Code Online (Sandbox Code Playgroud)

我正在以下模型中解码服务器响应

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

此代码无法处理401, 404来自服务器的错误响应等。所以我正在寻找的是将此 api ( URLRequest) 请求转换为Alamofire带有错误处理等的通用请求401, 404。我已经安装了Alamofirepod。有没有人开发了具有解码和错误处理Alamofire功能的通用请求方法?

提前致谢 :)

Sah*_*nda 8

Git 链接: https: //github.com/sahilmanchanda2/wrapper-class-for-alamofire

这是我的版本(使用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))
            }
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的类使用最新的 Alamofire 版本(截至 2020 年 2 月),该类涵盖了几乎所有 HTTP 方法,可以选择以应用程序/JSON 格式或普通格式发送数据。通过这个类,您可以获得很大的灵活性,它会自动将响应转换为您的 Swift 对象。

看一下这个类的init方法它有:

  1. data: [String,Any] = 您将在其中放置表单数据。

  2. headers: [String:String] = 在此您可以发送要与请求一起发送的自定义标头

  3. url = 这里你可以指定完整的url,如果你已经在Class中定义了baseurl,你可以将其留空。当您想要使用第三方提供的 REST 服务时,它会很方便。注意:如果您填写的是 url,那么下一个参数 service 应该为 nil

  4. service: services = 它是 NetworkClass 本身定义的枚举。这些作为端点。查看 init 方法,如果 url 为 nil 但服务不为 nil,那么它将附加在基本 url 的末尾以形成完整的 URL,将提供示例。

  5. method: HTTPMethod = 此处您可以指定请求应使用哪种 HTTP 方法。

  6. isJSONRequest = 默认设置为 true。如果你想发送普通请求,请将其设置为 false。

在 init 方法中,您还可以指定要随每个请求发送的通用数据或标头,例如您的应用程序版本号、iOS 版本等

现在看看执行方法:它是一个通用函数,如果响应成功,它将返回您选择的 swift 对象。如果无法将响应转换为 swift 对象,它将以字符串形式打印响应。如果响应代码不在 200-299 范围内,那么它将失败,并为您提供完整的调试描述以获取详细信息。

用法:

假设我们有以下结构:

struct Post: Codable{
    let userId: Int
    let id: Int
    let title: String
    let body: String
}
Run Code Online (Sandbox Code Playgroud)

请注意 NetworkClass https://jsonplaceholder.typicode.com/中定义的基本 url

示例 1:发送内容类型为 Application/JSON 的 HTTP Post

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)
            }
        }
Run Code Online (Sandbox Code Playgroud)

输出:

Service: posts 
data: ["userId": 1, "body": "bar", "title": "foo"]
Post(userId: 1, id: 101, title: "foo", body: "bar")
Run Code Online (Sandbox Code Playgroud)
  1. HTTP 400 请求

    NetworkCall(数据: ["email":"peter@klaven"], url: " https://reqres.in/api/login ", 方法: .post, isJSONRequest: false).executeQuery(){ (结果: 结果) 在 switch 结果{ case .success(let post): print(post) case .failure(let error): print(error) } }

输出:

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}
Run Code Online (Sandbox Code Playgroud)
  1. 带有自定义标头

    NetworkCall(数据:[“用户名”:“sahil.manchanda2@gmail.com”],标头:[“自定义标头键”:“自定义标头值”],网址:“ https://httpbin.org /post ", method: .post).executeQuery(){(result: Result) in switch result{ case .success(let data): print(data) case .failure(let error): print(error) } }

输出:

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))
Run Code Online (Sandbox Code Playgroud)

在最后一个示例中,您可以在末尾看到 typeMismatch,我尝试在executeQuery 中传递 [String:Any],但由于 Any 无法确认可编码,因此我不得不使用 String。