14

我使用一个 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 框架使用组合,所以我知道有一个发布者版本的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)

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

4

1 回答 1

37

你需要使用flatMap,而不是tryMap,周围dataTaskPublisher(for:)

看类型。从这个开始:

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

Option-clickp0以查看其推导类型。它是AnyPublisher<URLRequest, OAuthError>,因为那makeAuthenticatedRequest(request:)是声明返回的内容。

现在添加:

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

Option-clickp1以查看其推导类型,Publishers.TryMap<AnyPublisher<URLRequest, OAuthError>, URLSession.DataTaskPublisher>. 哎呀,这有点难以理解。通过使用来简化它eraseToAnyPublisher

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

现在推导出的类型p1AnyPublisher<URLSession.DataTaskPublisher, Error>。那仍然有一些神秘的类型URLSession.DataTaskPublisher,所以让我们也删除它:

let p1 = p0.tryMap {
    URLSession.shared.dataTaskPublisher(for: $0)
        .eraseToAnyPublisher() }
    .eraseToAnyPublisher()

现在 Xcode 可以告诉我们推导的类型p1AnyPublisher<AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>, OAuthError>. 让我重新格式化它以提高可读性:

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

它是发布发布者的发布者URLSession.DataTaskPublisher.Output

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

When you see this kind of nesting, with a publisher that publishes publishers, it means you probably needed to use flatMap instead of map (or tryMap). Let's do that:

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

Now we get a compile-time error:

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

The problem is that Combine can't flatten the nesting because the outer publisher's failure type is OAuthError and the inner publisher's failure type is URLError. Combine can only flatten them if they have the same failure type. We can fix this problem by converting both failure types to the general Error type:

let p1 = p0
    .mapError { $0 as Error }
    .flatMap {
        URLSession.shared.dataTaskPublisher(for: $0)
            .mapError { $0 as Error }
            .eraseToAnyPublisher() }
    .eraseToAnyPublisher()

This compiles, and Xcode tells us that the deduced type is AnyPublisher<URLSession.DataTaskPublisher.Output, Error>, which is what we want. We can tack on your next tryMap, but let's just use map instead because the body can't throw any errors:

let p2 = p1.map { $0.data }.eraseToAnyPublisher()

Xcode tells us p2 is an AnyPublisher<Data, Error>, so we could then chain a decode modifier.

Now that we have straightened out the types, we can get rid of all the type erasers and put it all together:

OAuthSession.current.makeAuthenticatedRequest(request: request)
    .mapError { $0 as Error }
    .flatMap {
        URLSession.shared.dataTaskPublisher(for: $0)
            .mapError { $0 as Error } }
    .map { $0.data }
    .decode(type: A.self, decoder: decoder)
于 2019-07-23T20:54:10.277 回答