Swift 组合链接 .mapError()

标签 swift combine

我正在尝试实现类似于下面呈现的场景(创建 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) }

我的问题:

  1. 是否有更好的方法使用组合中的链接来映射错误(并替换上面的行)?
  2. 有没有办法在链中包含第一个 guard letFail(error:)

最佳答案

我同意 iamtimmo 的观点,即您不需要 .subscribe(on:)。我还认为这个方法对于 .receive(on:) 来说是错误的地方,因为该方法中没有任何内容需要主线程。如果您在其他地方有代码订阅此发布者并希望在主线程上获得结果,那么您应该在其中使用 receive(on:) 运算符。我将在此答案中省略 .subscribe(on:).receive(on:)

无论如何,让我们解决您的问题。

  1. 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()
  1. Is there any way to include first guard let with Fail(error:) in chain?

是的,但同样不清楚这样做是否更好。在这种情况下,queryURL 的转换不是异步的,因此没有理由使用合并。但如果你真的想这样做,这里有一个方法:

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 而不是 mapquery 转换为 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/

相关文章:

ios - 不知何故结合搜索 Controller 不起作用,知道吗?

快速组合 : Merge publishers of different types

swift - 如何在 Swift 的图表中启用小数?

swift - 根据设备更改字体大小

ios - 格式化图表中的数字 ios swift

swift - 线程安全地将发布者组合到 AsyncStream

ios - UIView.label 上的 Swift/IBAction/CGRect

swift - 从解析破折号获取数据到数组

swift - iOS1 3's Combine streams don' t 运算符(operator)使用调度程序后的流程

快速组合 : How to create a single publisher from a list of publishers?