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?
CombineLatest publisher collects the first value from all three publishers and emits them as a single tuple. CombineLatest continues sending new values even when only one publisher emits a new value. On the other hand, the Zip operator sends a new value only when all the publishers emit new values.
Overview. A publisher delivers elements to one or more Subscriber instances. The subscriber's Input and Failure associated types must match the Output and Failure types declared by the publisher. The publisher implements the receive(subscriber:) method to accept a subscriber.
What's a sink? While a complete explanation of Combine, publishers, subscribers, and sinks is beyond the scope of this article, for our purposes here it's probably enough to know that in Combine a sink is the code receives data and completion events or errors from a publisher and deals with them.
Overview. The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With