Ios URLSession.shared.dataTask接收数据的正确方式

Ios URLSession.shared.dataTask接收数据的正确方式,ios,swift,api,error-handling,nsurlsessiondatatask,Ios,Swift,Api,Error Handling,Nsurlsessiondatatask,你好 在检查从dataTask接收到的数据(数据、响应、错误)和执行一些特殊的错误处理时,试图找到正确的顺序有点困惑 通常我们的URLSession如下所示: class HTTPRequest { static func request(urlStr: String, parameters: [String: String], completion: @escaping (_ data: Data?,_ response: URLResponse?, _ error: Error?)

你好

在检查从dataTask接收到的数据(数据、响应、错误)和执行一些特殊的错误处理时,试图找到正确的顺序有点困惑

通常我们的URLSession如下所示:

class HTTPRequest {
    static func request(urlStr: String, parameters: [String: String], completion: @escaping (_ data: Data?,_ response: URLResponse?, _ error: Error?) -> ()) {
        var url = OpenExchange.base_URL + urlStr
        url += getParameters(param: parameters)
        let request = URLRequest(url: URL(string: url)!)
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if error != nil {
                print("URLSession Error: \(String(describing: error?.localizedDescription))")
                completion(nil,nil,error)
            } else {
                completion(data,response,nil)
            }
        }
        task.resume()
    }

    static func getParameters(param: [String: String]) -> String {
        var data = [String]()
        for (key,value) in param {
            data.append(key + "=\(value)")
        }
        return data.map { String($0) }.joined(separator: "&")
    }

}
我还有一个函数,其中包含HTTPRequest,用于将所有内容包装到我正在使用的对象类型:

 static func networkOperation(urlStr: String, parameters: [String: String], completion: @escaping (ReturnedData) -> () ) {
        var recieved = ReturnedData()
        HTTPRequest.request(urlStr: urlStr, parameters: parameters) { (data, resp, err) in
            if let data = data, let response = resp {

// TODO: try JSONDecoder() if data is API Error Struct; Moderate this section depending on results of decoding;

                recieved.data = data
                recieved.response = response 
                recieved.result = .Success
                completion(recieved)
                return
            } else if err == nil {
                recieved.result = .ErrorUnknown
                completion(recieved)
                return
            }
            recieved.error = err as NSError?
            completion(recieved)
        }
       }

public struct ReturnedData {
    public var data: Data?
    public var response: URLResponse?
    public var error: Error?
    public var result: RequestResult = .ErrorHTTP
}

public enum RequestResult: String {
    case Success
    case ErrorAPI
    case ErrorHTTP
    case ErrorUnknown
}
使用上面的代码,我可以轻松地创建不同的networkOperation调用来执行不同的API方法,并处理返回的不同数据模型。我试图实现的是API错误检查。因为我的API有一些错误描述,例如当你的应用程序ID错误或者当前应用程序ID没有获取信息的权限等。。因此,如果出现上述任何情况,数据将如下所示:

  {
  "error": true,
  "status": 401,
  "message": "invalid_app_id",
  "description": "Invalid App ID provided - please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org."
  }

我认为尝试在networkOperations“//TODO”标记中使用错误struct对每个接收到的数据进行解码是不正确的,也许有一些好的方法来实现这一点

您应该让API错误返回错误对象

例如,你可以做:

enum NetworkRequestError: Error {
    case api(_ status: Int, _ code: ApiResultCode, _ description: String)
}
将响应编码到名为
apireultcode
enum
中,如下所示:

enum ApiResultCode {
    case invalidAppId
    case recordNotFound   // just an example
    ...
    case unknown(String)
}

extension ApiResultCode {
    static func code(for string: String) -> ApiResultCode {
        switch string {
        case "invalid_app_id":   return .invalidAppId
        case "record_not_found": return .recordNotFound
        ...
        default:                 return .unknown(string)
        }
    }
}
此枚举允许您检查
消息
代码,而不会在代码中添加字符串文字

如果你分析了一个API错误,你可以返回它。例如

if responseObject.error {
    let error = NetworkRequestError.api(responseObject.status, ApiResultCode.code(for: responseObject.message), responseObject.description)
    ... now pass this `error`, just like any other `Error` object
}

如果你愿意进行更广泛的重新设计,我个人建议

  • 重构
    RequestResult
    以提取这些单独的错误类型(调用者只想知道它是成功还是失败了……如果失败了,那么它应该查看
    error
    对象以确定失败的原因)
  • 但是让这个新的
    结果
    枚举包含相关值,即成功时的
    数据
    ,失败时的
    错误
    ;及
  • 现在,枚举在其关联值中包含了我们需要的内容,我们可以完全消除
    ReturnedData
因此,首先,让我们扩展该
RequestResult
,以包括失败时的错误和成功时的有效负载:

public enum Result {
    case success(Data)
    case failure(Error)
}
事实上,现代惯例是将此定义为通用,其中,使用以下方法将上述内容变成
结果

public enum Result<T, U> {
    case success(T)
    case failure(U)
}
因此,完成此操作后,您可以更改
请求
以传回
结果

这个
结果
泛型的美妙之处在于,它变成了一个可以在整个代码中使用的一致模式。例如,假设您有某种方法将从
请求
返回的
数据
中解析
Foo
对象:

func retrieveFoo(completion: @escaping (Result<Foo, Error>) -> Void) {
    request(...) { result in
        switch result {
        case .failure(let error):
            completion(.failure(error))

        case .success(let data):
            do {
                let responseObject = try JSONDecoder().decode(ResponseObject.self, from: data)
                if responseObject.error {
                    completion(.failure(NetworkRequestError.api(responseObject.status, ApiResultCode.code(for: responseObject.message), responseObject.description)))
                    return
                }

                let foo = responseObject.foo
                completion(.success(foo))
            } catch {
                completion(.failure(error))
            }
        }
    }
}

我会通过URL请求的
error
参数发送所有与服务器相关的错误。不相关的是,您的
getParameters
未对请求进行百分比编码。使用
URLComponents
正确转义百分比。看到了。谢谢你这么详细的回答,我马上就去尝试实现。真的让我恢复了对人性的信心。
static func request(urlString: String, parameters: [String: String], completion: @escaping (Result<Data, Error>) -> ()) {
    let request = URLRequest(url: URL(string: urlString)!)
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let responseData = data, error == nil else {
            completion(.failure(error ?? NetworkRequestError.unknown(data, response)))
            return
        }

        completion(.success(responseData))
    }
    task.resume()
}
request(...) { result in
    switch result {
    case .failure(let error):
        // do something with `error`

    case .success(let data):
        // do something with `data`
    }
}
func retrieveFoo(completion: @escaping (Result<Foo, Error>) -> Void) {
    request(...) { result in
        switch result {
        case .failure(let error):
            completion(.failure(error))

        case .success(let data):
            do {
                let responseObject = try JSONDecoder().decode(ResponseObject.self, from: data)
                if responseObject.error {
                    completion(.failure(NetworkRequestError.api(responseObject.status, ApiResultCode.code(for: responseObject.message), responseObject.description)))
                    return
                }

                let foo = responseObject.foo
                completion(.success(foo))
            } catch {
                completion(.failure(error))
            }
        }
    }
}
retrieveFoo { result in
    switch result {
    case .failure(NetworkRequestError.api(_, .recordNotFound, _)):
        // handle specific “record not found” error here

    case .failure(let error):
        // handle all other errors here

    case .success(let foo):
        // do something with `foo`
    }
}