问题
I use an OAuth framework which creates authenticated requests asynchronously like so:
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
// ...
}
// ...
}
}
I am trying to make my OAuth framework use Combine, so I know have a Publisher version of the makeAuthenticatedRequest
method i.e.:
public func makeAuthenticatedRequest(request: URLRequest) -> AnyPublisher<URLRequest, OAuthError>
I am trying to use this to replace the call site above like so:
OAuthSession.current.makeAuthenticatedRequestPublisher(request)
.tryMap(URLSession.shared.dataTaskPublisher(for:))
.tryMap { (data, _) in data } // Problem is here
.decode(type: A.self, decoder: decoder)
As noted above, the problem is on turning the result of the publisher into a new publisher. How can I go about doing this?
回答1:
You need to use flatMap
, not tryMap
, around dataTaskPublisher(for:)
.
Look at the types. Start with this:
let p0 = OAuthSession.current.makeAuthenticatedRequest(request: request)
Option-click on p0
to see its deduced type. It is AnyPublisher<URLRequest, OAuthError>
, since that is what makeAuthenticatedRequest(request:)
is declared to return.
Now add this:
let p1 = p0.tryMap(URLSession.shared.dataTaskPublisher(for:))
Option-click on p1
to see its deduced type, Publishers.TryMap<AnyPublisher<URLRequest, OAuthError>, URLSession.DataTaskPublisher>
. Oops, that's a little hard to understand. Simplify it by using eraseToAnyPublisher
:
let p1 = p0
.tryMap(URLSession.shared.dataTaskPublisher(for:))
.eraseToAnyPublisher()
Now the deduced type of p1
is AnyPublisher<URLSession.DataTaskPublisher, Error>
. That still has the somewhat mysterious type URLSession.DataTaskPublisher
in it, so let's erase that too:
let p1 = p0.tryMap {
URLSession.shared.dataTaskPublisher(for: $0)
.eraseToAnyPublisher() }
.eraseToAnyPublisher()
Now Xcode can tell us that the deduced type of p1
is AnyPublisher<AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>, OAuthError>
. Let me reformat that for readability:
AnyPublisher<
AnyPublisher<
URLSession.DataTaskPublisher.Output,
URLSession.DataTaskPublisher.Failure>,
OAuthError>
It's a publisher that publishes publishers that publish URLSession.DataTaskPublisher.Output
.
That's not what you expected, and it's why your second tryMap
fails. You thought you were creating a publisher of URLSession.DataTaskPublisher.Output
(which is a typealias
for the tuple (data: Data, response: URLResponse)
), and that's the input your second tryMap
wants. But Combine thinks your second tryMap
's input should be a 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)
来源:https://stackoverflow.com/questions/57153221/combine-turn-one-publisher-into-another