Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combine turn one Publisher into another

Tags:

ios

swift

combine

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?

like image 257
jjatie Avatar asked Jul 22 '19 20:07

jjatie


People also ask

How do I combine two publishers?

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.

What is publisher in combine?

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 is sink in combine?

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.

What is swift combine?

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.


1 Answers

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)
like image 118
rob mayoff Avatar answered Oct 16 '22 04:10

rob mayoff