URLSession.shared.dataTask правильный способ получения данных - PullRequest
0 голосов
/ 02 марта 2019

Добрый день!

Я немного запутался, пытаясь найти правильную последовательность при проверке полученных (data, response, 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 есть некоторое описание ошибки, например, когда вы неправильно указали свой APP_ID или у текущего APP_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."
  }

Iдумаю, что не все в порядке, пытаясь декодировать все полученные данные со структурой Error в метке networkOperations "// TODO", может быть, есть какой-то хороший способ реализовать это?

1 Ответ

0 голосов
/ 02 марта 2019

Ваши ошибки API должны возвращать объекты ошибок.

Например, вы можете сделать:

enum NetworkRequestError: Error {
    case api(_ status: Int, _ code: ApiResultCode, _ description: String)
}

Где вы кодируете свои ответы в enum с именем ApiResultCode, например так:

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 коды без засорения вашего кода строковыми литералами.

И если вы анализируете ошибку 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, чтобы определить причину сбоя);
  • , но включить в него новое перечисление Resultзначения, а именно Data при успехе и Error при неудаче;и
  • теперь, когда перечисление включает в себя то, что нам нужно в связанных с ним значениях, мы можем полностью исключить ReturnedData.

Итак, сначала давайте расширим этот RequestResult, включив в негоошибка при сбоях и полезная нагрузка при успехе:

public enum Result {
    case success(Data)
    case failure(Error)
}

На самом деле, современное соглашение состоит в том, чтобы сделать этот универсальный, где вышеприведенное становится Result<Data, Error>, используя следующее:

public enum Result<T, U> {
    case success(T)
    case failure(U)
}

(Swift 5 на самом деле включает в себя этот универсальный.)

И тогда я бы расширил ResultError для обработки как ошибок API, так и любых неизвестных ошибок:

enum NetworkRequestError: Error {
    case api(_ status: Int, _ code: ApiResultCode, _ description: String)
    case unknown(Data?, URLResponse?)
}

Итак, сделав это,Вы можете изменить request, чтобы передать обратно 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 общего в том, что онстановится последовательным шаблоном, который вы можете использовать в своем коде.Например, предположим, у вас есть какой-то метод, который собирается проанализировать Foo объект из Data, который request вернул:

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:

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`
    }
}
...