合并将一个 Publisher 变成另一个 Publisher

Combine turn one Publisher into another

我使用 OAuth 框架异步创建经过身份验证的请求,如下所示:

OAuthSession.current.makeAuthenticatedRequest(request: myURLRequest) { (result: Result<URLRequest, OAuthError>) in
            switch result {
            case .success(let request):
                URLSession.shared.dataTask(with: request) { (data, response, error) in
                    // ...
                }
             // ...
             }
        }

我正在尝试让我的 OAuth 框架使用 Combine,所以我知道有一个发布者版本的 makeAuthenticatedRequest 方法,即:

public func makeAuthenticatedRequest(request: URLRequest) -> AnyPublisher<URLRequest, OAuthError>

我正在尝试使用它来替换上面的调用站点,如下所示:

OAuthSession.current.makeAuthenticatedRequestPublisher(request)
    .tryMap(URLSession.shared.dataTaskPublisher(for:))
    .tryMap { (data, _) in data } // Problem is here
    .decode(type: A.self, decoder: decoder)

如上所述,问题在于将发布者的结果转换为新的发布者。我该怎么做呢?

您需要在 dataTaskPublisher(for:) 左右使用 flatMap,而不是 tryMap

看类型。从这个开始:

let p0 = OAuthSession.current.makeAuthenticatedRequest(request: request)

Option-click on p0 查看其推导类型。它是 AnyPublisher<URLRequest, OAuthError>,因为那是 makeAuthenticatedRequest(request:) 被声明为 return。

现在添加:

let p1 = p0.tryMap(URLSession.shared.dataTaskPublisher(for:))

Option-click 在 p1 上查看其推导类型 Publishers.TryMap<AnyPublisher<URLRequest, OAuthError>, URLSession.DataTaskPublisher>。哎呀,有点难以理解。使用 eraseToAnyPublisher:

简化它
let p1 = p0
    .tryMap(URLSession.shared.dataTaskPublisher(for:))
    .eraseToAnyPublisher()

现在 p1 的推导类型是 AnyPublisher<URLSession.DataTaskPublisher, Error>。它仍然有一些神秘的类型URLSession.DataTaskPublisher,所以我们也把它擦掉:

let p1 = p0.tryMap {
    URLSession.shared.dataTaskPublisher(for: [=13=])
        .eraseToAnyPublisher() }
    .eraseToAnyPublisher()

现在Xcode可以告诉我们p1的推导类型是AnyPublisher<AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>, OAuthError>。为了便于阅读,让我重新格式化它:

AnyPublisher<
    AnyPublisher<
        URLSession.DataTaskPublisher.Output, 
        URLSession.DataTaskPublisher.Failure>,
    OAuthError>

这是一个出版商,出版商出版URLSession.DataTaskPublisher.Output

这不是您所期望的,这就是您的第二个 tryMap 失败的原因。您认为您正在创建 URLSession.DataTaskPublisher.Output 的发布者(这是元组 (data: Data, response: URLResponse)typealias),这就是您的第二个 tryMap 想要的输入。但是 Combine 认为您的第二个 tryMap 的输入应该是 URLSession.DataTaskPublisher.

当您看到这种嵌套,发布者发布发布者时,这意味着您可能需要使用 flatMap 而不是 map(或 tryMap)。让我们这样做:

let p1 = p0.flatMap {
       //   ^^^^^^^ flatMap instead of tryMap
    URLSession.shared.dataTaskPublisher(for: [=15=])
        .eraseToAnyPublisher() }
    .eraseToAnyPublisher()

现在我们得到一个 compile-time 错误:

Instance method 'flatMap(maxPublishers:_:)' requires the types 'OAuthError' and 'URLSession.DataTaskPublisher.Failure' (aka 'URLError') be equivalent

问题在于 Combine 无法展平嵌套,因为外部发布者的失败类型是 OAuthError 而内部发布者的失败类型是 URLError。如果它们具有相同的故障类型,Combine 只能将它们展平。我们可以通过将两种故障类型转换为通用 Error 类型来解决此问题:

let p1 = p0
    .mapError { [=16=] as Error }
    .flatMap {
        URLSession.shared.dataTaskPublisher(for: [=16=])
            .mapError { [=16=] as Error }
            .eraseToAnyPublisher() }
    .eraseToAnyPublisher()

这样编译,Xcode告诉我们推导的类型是AnyPublisher<URLSession.DataTaskPublisher.Output, Error>,这就是我们想要的。我们可以添加您的下一个 tryMap,但我们只使用 map,因为正文不会抛出任何错误:

let p2 = p1.map { [=17=].data }.eraseToAnyPublisher()

Xcode 告诉我们 p2 是一个 AnyPublisher<Data, Error>,因此我们可以链接一个 decode 修饰符。

现在我们理清了类型,我们可以去掉所有的类型橡皮擦,把它们放在一起:

OAuthSession.current.makeAuthenticatedRequest(request: request)
    .mapError { [=18=] as Error }
    .flatMap {
        URLSession.shared.dataTaskPublisher(for: [=18=])
            .mapError { [=18=] as Error } }
    .map { [=18=].data }
    .decode(type: A.self, decoder: decoder)