我正在尝试实现类似于下面呈现的场景(创建 URL、向服务器请求、解码 json、自定义 NetworkError
枚举中包含的每个步骤的错误):
enum NetworkError: Error {
case badUrl
case noData
case request(underlyingError: Error)
case unableToDecode(underlyingError: Error)
}
//...
func searchRepos(with query: String, success: @escaping (ReposList) -> Void, failure: @escaping (NetworkError) -> Void) {
guard let url = URL(string: searchUrl + query) else {
failure(.badUrl)
return
}
session.dataTask(with: url) { data, response, error in
guard let data = data else {
failure(.noData)
return
}
if let error = error {
failure(.request(underlyingError: error))
return
}
do {
let repos = try JSONDecoder().decode(ReposList.self, from: data)
DispatchQueue.main.async {
success(repos)
}
} catch {
failure(.unableToDecode(underlyingError: error))
}
}.resume()
}
我的组合解决方案有效:
func searchRepos(with query: String) -> AnyPublisher<ReposList, NetworkError> {
guard let url = URL(string: searchUrl + query) else {
return Fail(error: .badUrl).eraseToAnyPublisher()
}
return session.dataTaskPublisher(for: url)
.mapError { NetworkError.request(underlyingError: $0) }
.map { $0.data }
.decode(type: ReposList.self, decoder: JSONDecoder())
.mapError { $0 as? NetworkError ?? .unableToDecode(underlyingError: $0) }
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
但是我真的不喜欢这句话
.mapError { $0 as? NetworkError ?? .unableToDecode(underlyingError: $0) }
我的问题:
- 是否有更好的方法使用组合中的链接来映射错误(并替换上面的行)?
- 有没有办法在链中包含第一个
guard let
和Fail(error:)
?
最佳答案
我同意 iamtimmo 的观点,即您不需要 .subscribe(on:)
。我还认为这个方法对于 .receive(on:)
来说是错误的地方,因为该方法中没有任何内容需要主线程。如果您在其他地方有代码订阅此发布者并希望在主线程上获得结果,那么您应该在其中使用 receive(on:)
运算符。我将在此答案中省略 .subscribe(on:)
和 .receive(on:)
。
无论如何,让我们解决您的问题。
- Is there better way to map errors (and replace line above) using chaining in Combine?
“更好”是主观的。您在这里试图解决的问题是您只想将 mapError
应用于由 decode(type:decoder:)
运算符产生的错误。您可以使用 flatMap
运算符在完整管道内创建一个迷你管道:
return session.dataTaskPublisher(for: url)
.mapError { NetworkError.request(underlyingError: $0) }
.map { $0.data }
.flatMap {
Just($0)
.decode(type: ReposList.self, decoder: JSONDecoder())
.mapError { .unableToDecode(underlyingError: $0) } }
.eraseToAnyPublisher()
这“更好”吗?嗯。
您可以将迷你管道提取到新版本的解码
中:
extension Publisher {
func decode<Item, Coder>(type: Item.Type, decoder: Coder, errorTransform: @escaping (Error) -> Failure) -> Publishers.FlatMap<Publishers.MapError<Publishers.Decode<Just<Self.Output>, Item, Coder>, Self.Failure>, Self> where Item : Decodable, Coder : TopLevelDecoder, Self.Output == Coder.Input {
return flatMap {
Just($0)
.decode(type: type, decoder: decoder)
.mapError { errorTransform($0) }
}
}
}
然后像这样使用它:
return session.dataTaskPublisher(for: url)
.mapError { NetworkError.request(underlyingError: $0) }
.map { $0.data }
.decode(
type: ReposList.self,
decoder: JSONDecoder(),
errorTransform: { .unableToDecode(underlyingError: $0) })
.eraseToAnyPublisher()
- Is there any way to include first
guard let
withFail(error:)
in chain?
是的,但同样不清楚这样做是否更好。在这种情况下,query
到 URL
的转换不是异步的,因此没有理由使用合并。但如果你真的想这样做,这里有一个方法:
return Just(query)
.setFailureType(to: NetworkError.self)
.map { URL(string: searchUrl + $0).map { Result.success($0) } ?? Result.failure(.badUrl) }
.flatMap { $0.publisher }
.flatMap {
session.dataTaskPublisher(for: $0)
.mapError { .request(underlyingError: $0) } }
.map { $0.data }
.decode(
type: ReposList.self,
decoder: JSONDecoder(),
errorTransform: { .unableToDecode(underlyingError: $0) })
.eraseToAnyPublisher()
这很复杂,因为合并没有任何运算符可以将正常输出或完成转换为类型化失败。它有 tryMap
和类似的,但这些都会产生 Failure
类型的 Error
而不是任何更具体的内容。
我们可以编写一个运算符,将空流转换为特定错误:
extension Publisher where Failure == Never {
func replaceEmpty<NewFailure: Error>(withFailure failure: NewFailure) -> Publishers.FlatMap<Result<Self.Output, NewFailure>.Publisher, Publishers.ReplaceEmpty<Publishers.Map<Publishers.SetFailureType<Self, NewFailure>, Result<Self.Output, NewFailure>>>> {
return self
.setFailureType(to: NewFailure.self)
.map { Result<Output, NewFailure>.success($0) }
.replaceEmpty(with: Result<Output, NewFailure>.failure(failure))
.flatMap { $0.publisher }
}
}
现在我们可以使用 compactMap
而不是 map
将 query
转换为 URL
,生成一个空流如果我们无法创建 URL
,并使用 new 运算符将空流替换为 .badUrl
错误:
return Just(query)
.compactMap { URL(string: searchUrl + $0) }
.replaceEmpty(withFailure: .badUrl)
.flatMap {
session.dataTaskPublisher(for: $0)
.mapError { .request(underlyingError: $0) } }
.map { $0.data }
.decode(
type: ReposList.self,
decoder: JSONDecoder(),
errorTransform: { .unableToDecode(underlyingError: $0) })
.eraseToAnyPublisher()
关于Swift 组合链接 .mapError(),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57768427/