Swift 组合链接 .mapError()

Mic*_*cio 10 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()
    }
Run Code Online (Sandbox Code Playgroud)

我在结合工作的解决方案:

    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()
    }
Run Code Online (Sandbox Code Playgroud)

但我真的不喜欢这条线

.mapError { $0 as? NetworkError ?? .unableToDecode(underlyingError: $0) }
Run Code Online (Sandbox Code Playgroud)

我的问题:

  1. 有没有更好的方法来映射错误(并替换上面的行)在组合中使用链接?
  2. 有没有什么办法,包括第一次guard letFail(error:)连锁?

rob*_*off 10

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

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

  1. 有没有更好的方法来映射错误(并替换上面的行)在组合中使用链接?

“更好”是主观的。您在这里尝试解决的问题是您只想将其应用于操作员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()
Run Code Online (Sandbox Code Playgroud)

这是否更好”?嗯。

您可以将迷你管道提取到以下版本的新版本中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) }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后像这样使用它:

return session.dataTaskPublisher(for: url)
    .mapError { NetworkError.request(underlyingError: $0) }
    .map { $0.data }
    .decode(
        type: ReposList.self,
        decoder: JSONDecoder(),
        errorTransform: { .unableToDecode(underlyingError: $0) })
    .eraseToAnyPublisher()
Run Code Online (Sandbox Code Playgroud)
  1. 有没有什么办法,包括第一次guard letFail(error:)连锁?

是的,但同样不清楚这样做是否更好。在这种情况下,转换query为 aURL不是异步的,因此几乎没有理由使用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()
Run Code Online (Sandbox Code Playgroud)

这是令人费解的,因为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 }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以使用compactMap,而不是mapqueryURL,产生一个空的流,如果我们不能创建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()
Run Code Online (Sandbox Code Playgroud)