#swift #dictionary #generics #combine
#swift #словарь #обобщения #объединение
Вопрос:
Мой API возвращает этот формат, где data
могут содержаться все виды ответов.
{
status: // http status
error?: // error handle
data?: // your response data
meta?: // meta data, eg. pagination
debug?: // debuging infos
}
Я создал Codable
тип ответа с общим значением для необязательных данных, тип которых нам неизвестен.
struct MyResponse<T: Codable>: Codable {
let status: Int
let error: String?
let data: T?
let meta: Paging?
let debug: String?
}
Сейчас я пытаюсь написать удобные методы API как можно лаконичнее. Итак, у меня есть функция для возврата универсального издателя, который я могу использовать для всех этих ответов, то есть та, которая предварительно анализирует ответ и заранее выявляет любые ошибки.
Сначала я получаю dataTaskPublisher
, который обрабатывает входные параметры, если таковые имеются. Endpoint
это просто удобство String
enum
для моих конечных точек, Method
аналогично. MyRequest
возвращает URLRequest
с некоторыми необходимыми заголовками и т.д.
Обратите внимание на то, как я определяю параметры: params: [String:T]
. Это стандартный JSON, поэтому это могут быть строки, числа и т.д.
Похоже, что в этом T
и заключается проблема каким-то образом..
static fileprivate func publisher<T: Encodable>(
_ path: Endpoint,
method: Method,
params: [String:T] = [:]) throws
-> URLSession.DataTaskPublisher
{
let url = API.baseURL.appendingPathComponent(path.rawValue)
var request = API.MyRequest(url: url)
if method == .POST amp;amp; params.count > 0 {
request.httpMethod = method.rawValue
do {
let data = try JSONEncoder().encode(params)
request.httpBody = data
return URLSession.shared.dataTaskPublisher(for: request)
}
catch let err {
throw MyError.encoding(description: String(describing: err))
}
}
return URLSession.shared.dataTaskPublisher(for: request)
}
Далее я анализирую ответ.
static func myPublisher<T: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: [String:T] = [:])
-> AnyPublisher<MyResponse<R>, MyError>
{
do {
return try publisher(path, method: method, params: params)
.map(.data)
.mapError { MyError.network(description: "($0)")}
.decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
.mapError { MyError.encoding(description: "($0)")} //(2)
.tryMap {
if $0.status > 204 {
throw MyError.network(description: "($0.status): ($0.error!)")
}
else {
return $0 // returns a MyResponse
}
}
.mapError { $0 as! MyError }
//(1)
.eraseToAnyPublisher()
}
catch let err {
return Fail<MyResponse<R>,MyError>(error: err as? MyError ??
MyError.undefined(description: "(err)"))
.eraseToAnyPublisher()
}
}
Теперь я могу легко написать метод конечной точки. Вот два примера.
static func documents() -> AnyPublisher<[Document], MyError> {
return myPublisher(.documents)
.map(.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<[Document], MyError>
}
и
static func user() -> AnyPublisher<User, MyError> {
return myPublisher(.user)
.map(.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<User, MyError>
}
Все это работает хорошо. Пожалуйста, обратите внимание, что каждый раз мне приходится дважды указывать свой точный возвращаемый тип. Я думаю, что смогу с этим смириться.
Я должен быть в состоянии упростить это, чтобы мне не приходилось каждый раз повторять одни и те же три оператора (map, mapError, receive) точно таким же образом.
Но когда я вставляю .map(.data!)
в расположении //(1)
выше, я получаю ошибку Generic parameter T could not be inferred.
в расположении //(2)
.
Это действительно сбивает с толку. Почему универсальный тип во входных параметрах играет здесь какую-либо роль? Это должно быть связано с вызовом .decode
оператора чуть выше, где вызывается рассматриваемый универсальный код R
, а не T
.
Можете ли вы это объяснить? Как я могу выполнить рефакторинг этих операторов выше по потоку?
Комментарии:
1. Какими типами T могли бы быть, и как бы вы справились с этим JSON, если бы не использовали Combine?
2. T — это все типы, разрешенные json, но, по сути,
String
,Int
иFloat
. Я справляюсь с этим так, как описано выше, это работает нормально.3. О, я вижу проблему. Вас просто смутило сообщение об ошибке. Combine постоянно выдает эту ошибку компиляции! Ничего общего с вашим дженериком. Смотрите мои рекомендации по успешному редактированию кода Combine: apeth.com/UnderstandingCombine/tricksandtips.html
4. Но я не могу заставить это работать… Почему я не могу выполнить фильтрацию дальше по
data
полю? Вводом должен быть файл,MyResponse
который имеет необязательныйdata
путь к ключу. После завершения всего (перехват ошибки, получение в основной очереди) ошибка все еще существует. Что дальше?5. Обратите особое внимание на раздел «Укажите явный тип возвращаемого значения». Но прочитайте это целиком. Следуйте моей методике, вы поймете это.
Ответ №1:
В этом коде есть ряд небольших проблем. Вы правы, что это так [String: T]
. Это означает, что для данного набора параметров все значения должны быть одного типа. Это не «JSON». Это будет принимать [String: String]
или a [String: Int]
, но вы не сможете иметь оба значения Int и String в одном словаре, если вы сделаете это таким образом. И он также будет принят [String: Document]
, и не похоже, что вы действительно этого хотите.
Я бы рекомендовал переключить это на просто Encodable, что позволило бы вам передавать структуры, если это было удобно, или словари, если это было удобно:
func publisher<Params: Encodable>(
_ path: Endpoint,
method: Method,
params: Params?) throws
-> URLSession.DataTaskPublisher
func myPublisher<Params: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: Params?)
-> AnyPublisher<MyResponse<R>, MyError>
Затем измените свой params.count
, чтобы вместо этого проверять значение nil.
Обратите внимание, что я не создавал params = nil
параметр по умолчанию. Это потому, что это воссоздало бы вторую проблему, которая у вас есть. T
(и параметры) не могут быть выведены в случае по умолчанию. Для = [:]
чего T
? Swift должен знать, даже если он пуст. Таким образом, вместо значения по умолчанию вы используете перегрузку:
func myPublisher<R: Decodable>(
_ path: Endpoint,
method: Method = .GET)
-> AnyPublisher<MyResponse<R>, MyError> {
let params: String? = nil // This should be `Never?`, see https://twitter.com/cocoaphony/status/1184470123899478017
return myPublisher(path, method: method, params: params)
}
Теперь, когда вы не передаете никаких параметров, Params автоматически становится строкой.
Итак, теперь ваш код в порядке, и вам не нужно as
в конце
func documents() -> AnyPublisher<[Document], MyError> {
myPublisher(.documents)
.map(.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() // <== Removed `as ...`
}
Теперь это .map(.data!)
меня расстраивает. Если вы получите поврежденные данные с сервера, приложение завершит работу. Существует множество веских причин для аварийного завершения работы приложений; неверные данные сервера никогда не являются одной из них. Но исправление этого на самом деле не связано с этим вопросом (и немного сложно, потому что типы сбоев, отличные от Error, в настоящее время усложняют работу), поэтому я пока оставлю это. Моя общая рекомендация — использовать Error в качестве типа сбоя и допускать появление неожиданных ошибок, а не помещать их в .undefined
case. Если вам все равно нужно какое-то универсальное «другое», вы могли бы также сделать это с types («is»), а не с дополнительным регистром перечисления (который просто переводит «is» в переключатель). По крайней мере, я бы перенес сопоставление Error-> MyError как можно позже, что значительно упростит обработку этого.
Еще одна настройка, чтобы сделать последующие вещи немного более общими, я подозреваю, что MyResponse должен быть только декодируемым, а не кодируемым (остальное работает в любом случае, но это делает его немного более гибким):
struct MyResponse<T: Decodable>: Decodable { ... }
И на ваш первоначальный вопрос о том, как сделать это повторно используемым, теперь вы можете извлечь универсальную функцию:
func fetch<DataType, Params>(_: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET,
params: Params?) -> AnyPublisher<DataType, MyError>
where DataType: Decodable, Params: Encodable
{
myPublisher(endpoint, method: method, params: params)
.map(.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
// Overload to handle no parameters
func fetch<DataType>(_ dataType: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET) -> AnyPublisher<DataType, MyError>
where DataType: Decodable
{
fetch(dataType, from: endpoint, method: method, params: nil as String?)
}
func documents() -> AnyPublisher<[Document], MyError> {
fetch([Document].self, from: .documents)
}
Комментарии:
1. Спасибо, это выглядит действительно хорошо. 1. Я реализовал
Params?
предложение, конечно, это имеет гораздо больше смысла. 2. Я пытаюсь избежать недостатковdata!
, сначала проверяяnil
наличие и выдавая ошибку. 3. Я уже использовал перегрузку, для удобства, если нет параметров. 4. ПередачаDataType.Type
функции — хорошая идея, это делает вызов API еще более кратким. Довольно элегантно.