【问题标题】:Swift Combine chaining .mapError()Swift 组合链接 .mapError()
【发布时间】:2019-09-03 09:06:04
【问题描述】:

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

我在 Combine 中的解决方案有效:

    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. 有没有更好的方法在 Combine 中使用链接来映射错误(并替换上面的行)?
  2. 有没有办法在链中包含第一个 guard letFail(error:)

【问题讨论】:

    标签: swift combine


    【解决方案1】:

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

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

    1. 有没有更好的方法在 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()
    

    这是“更好”吗?嗯。

    您可以将迷你管道提取到新版本的decode

    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. 有没有办法在链中包含第一个 guard letFail(error:)

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

    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()
    

    这很复杂,因为 Combine 没有任何运算符可以将正常输出或完成转换为类型化失败。它有 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,则生成一个空流,并使用我们的新运算符替换空的带有.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()
    

    【讨论】:

    • 我已经使用示例 API url 尝试了上面的第一个代码块,我将 API url 更改为返回 404 http 错误的内容(在 Postman 应用程序中调用它时确实返回 404)但是.mapError { NetworkError.request(underlyingError: $0) } 行永远不会将错误传播到接收器运算符的完成块,报告的错误始终是:.unableToDecode(underlyingError: $0) 所以报告的错误实际上是错误的。解决这个问题,将.mapError 行替换为tryMap 运算符,该运算符将URLError 抛出到Apple Docs 中提到的下一个管道运算符。
    【解决方案2】:

    我不认为你的方法是不合理的。第一个mapError()// 1)的好处是您不需要了解请求中可能出现的错误。

        return session.dataTaskPublisher(for: url)
            .mapError { NetworkError.request(underlyingError: $0) }   // 1
            .map { $0.data }
            .decode(type: ReposList.self, decoder: JSONDecoder())
            .mapError { $0 as? NetworkError ?? .unableToDecode(underlyingError: $0) }
            .subscribe(on: DispatchQueue.global())   // 2 - not needed
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
        }
    

    我认为您不需要// 2 上的subscribe(on:),因为 URLSession.DataTaskPublisher 已经在后台线程上启动。后面的receive(on:) 是必需的。

    另一种方法是先运行“快乐路径”,然后再映射所有错误,如下所示。您需要了解哪些错误来自哪些发布商/运营商才能正确映射到您的 NetworkError 枚举。

        return session.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: ReposList.self, decoder: JSONDecoder())
            .mapError({ error -> NetworkError in
                // map all the errors here
            })
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    

    要处理您的第二个问题,您可以使用 tryMap()flatMap() 将您的 query 映射到 URL,然后映射到 URLSession.DataTaskPublisher 实例。我还没有测试过这个特定的代码,但是按照这些思路可以找到一个解决方案。

        Just(query)
            .tryMap({ query in
                guard let url = URL(string: searchUrl + query) else { throw NetworkError.badUrl }
                return url
            })
            .flatMap({ url in
                URLSession.shared.dataTaskPublisher(for: url)
                    .mapError { $0 as Error }
            })
            .map { $0.data }
            //
            // ... operators from the previous examples
            //
            .eraseToPublisher()
    

    【讨论】:

      猜你喜欢
      • 2012-06-16
      • 2016-03-24
      • 2019-10-29
      • 2018-06-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多