11

我正在尝试实现类似于下面呈现的场景(创建 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 let包含Fail(error:)在链中?
4

2 回答 2

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

这是否更好”?嗯。

您可以将迷你管道提取到新版本中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 let包含Fail(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()

这是令人费解的,因为 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而不是map变成querya URL,如果我们不能创建 a ,则生成一个空流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()
于 2020-01-14T19:26:13.297 回答
2

我不认为你的方法是不合理的。mapError()第一个(at )的好处// 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()
    }

我认为您不需要subscribe(on:)at // 2,因为 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()andflatMap()将您映射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()
于 2020-01-14T17:54:28.503 回答