问题描述
美好的一天!
在检查从 dataTask 接收到的(数据、响应、错误)并进行一些特殊的错误处理时,我有点困惑.
I am a little confused in trying to find the correct sequence in checking received (data, response, error) from dataTask and doing some special error handling.
通常我们的 URLSession 如下所示:
Usually we have URLSession looking like this:
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 的函数,用于将所有内容包装到我正在使用的对象类型:
I have another function that has HTTPRequest inside of it, to wrap everything to and object type I'm working with:
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 有一些错误描述,例如当您获取 APP_ID 错误或当前 APP_ID 无权获取信息等时.因此,如果其中任何一个发生,数据将如下所示:
Using the code above I can easily create different networkOperation calls for doing different API methods and handle different Data models that are returned. What I am trying to implement is API Error check. Since my API has some error description for example when you get your APP_ID wrong or current APP_ID has no permission to get information etc.. So if any of these occur the data will look like this:
{
"error": true,
"status": 401,
"message": "invalid_app_id",
"description": "Invalid App ID provided - please sign up at https://openexchangerates.org/signup, or contact [email protected]."
}
我认为尝试用 networkOperations "//TODO" 标记中的错误结构解码每个接收到的数据是不行的,也许有一些好的方法来实现这一点?
I think its not alright trying to decode every received data with Error struct in networkOperations "//TODO" mark, maybe there is some good way to implement this?
推荐答案
您应该让 API 错误返回错误对象.
You should have your API errors return error objects.
例如你可以这样做:
enum NetworkRequestError: Error {
case api(_ status: Int, _ code: ApiResultCode, _ description: String)
}
您将响应编码到一个名为 ApiResultCode
的 enum
中,如下所示:
Where you code your responses into an enum
called ApiResultCode
like so:
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)
}
}
}
这个枚举可以让你检查 message
代码,而不会用字符串文字乱扔你的代码.
This enum lets you check message
codes without littering your code with string literals.
如果您解析 API 错误,您可以返回该错误.例如
And if you parse an API error, you could return that. E.g.
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
}
如果您愿意接受更广泛的重新设计,我个人建议
If you’re open to a broader redesign, I’d personally suggest
- 重构
RequestResult
以提取那些单独的错误类型(调用者只想知道它是成功还是失败......如果失败,它应该查看Error
code> 对象以确定失败的原因); - 但是让这个新的
Result
枚举包含相关值,即成功时的Data
和失败时的Error
;和 - 既然枚举在其关联值中包含了我们需要的内容,我们可以完全消除
ReturnedData
.
- refactor
RequestResult
to pull out those individual error types (the caller wants to know simply if it succeeded or failed ... if it failed, it should then look at theError
object to determine why it failed); - but have this new
Result
enumeration include associated values, namely theData
on success and theError
on failure; and - now that the enumeration includes what we need in its associated values, we can completely eliminate
ReturnedData
.
所以,首先,让我们扩展 RequestResult
以包含失败时的错误和成功时的负载:
So, first, let’s expand that RequestResult
to include the error on failures and the payload on success:
public enum Result {
case success(Data)
case failure(Error)
}
实际上,现代惯例是使此通用,其中使用以下内容将上述内容变为 Result
:
Actually, modern convention is to make this generic, where the above becomes a Result<Data, Error>
using the following:
public enum Result<T, U> {
case success(T)
case failure(U)
}
(Swift 5 实际上包含这个泛型.)
(Swift 5 actually includes this generic.)
然后我会扩展 ResultError
来处理 API 错误以及任何未知错误:
And I’d then expand ResultError
to handle both API errors as well as any unknown errors:
enum NetworkRequestError: Error {
case api(_ status: Int, _ code: ApiResultCode, _ description: String)
case unknown(Data?, URLResponse?)
}
因此,完成此操作后,您可以更改 request
以传回 Result
:
So, having done this, you can change request
to pass back a Result<Data, Error>
:
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`
}
}
这个 Result
泛型的美妙之处在于它成为了一个可以在整个代码中使用的一致模式.例如,假设您有一些方法将从 request
返回的 Data
中解析一个 Foo
对象:
The beauty of this Result
generic is that it becomes a consistent pattern you can use throughout your code. For example, let’s assume you have some method that is going to parse a Foo
object out of the Data
that request
returned:
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))
}
}
}
}
或者,如果您想测试特定的 API 错误,例如.recordNotFound
:
Or, if you wanted to test for a particular API error, e.g. .recordNotFound
:
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`
}
}
这篇关于URLSession.shared.dataTask 接收数据的正确方式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!