Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert URLSession.DataTaskPublisher to Future publisher

Tags:

ios

swift

combine

How to convert URLSession.DataTaskPublisher to Future in Combine framework. In my opinion, the Future publisher is more appropriate here because the call can emit only one response and fails eventually.

In RxSwift there is helper method like asSingle.

I have achieved this transformation using the following approach but have no idea if this is the best method.

        return Future<ResponseType, Error>.init { (observer) in
        self.urlSession.dataTaskPublisher(for: urlRequest)
            .tryMap { (object) -> Data in
            //......
            }
            .receive(on: RunLoop.main)
            .sink(receiveCompletion: { (completion) in
                if case let .failure(error) = completion {
                    observer(.failure(error))
                }
            }) { (response) in
                observer(.success(response))
            }.store(in: &self.cancellable)
    }
}

Is there any easy way to do this?

like image 696
mkowal87 Avatar asked Feb 27 '20 07:02

mkowal87


2 Answers

As I understand it, the reason to use .asSingle in RxSwift is that, when you subscribe, your subscriber receives a SingleEvent which is either a .success(value) or a .error(error). So your subscriber doesn't have to worry about receiving a .completion type of event, because there isn't one.

There is no equivalent to that in Combine. In Combine, from the subscriber's point of view, Future is just another sort of Publisher which can emit output values and a .finished or a .failure(error). The type system doesn't enforce the fact that a Future never emits a .finished.

Because of this, there's no programmatic reason to return a Future specifically. You could argue that returning a Future documents your intent to always return either exactly one output, or a failure. But it doesn't change the way you write your subscriber.

Furthermore, because of Combine's heavy use of generics, as soon as you want to apply any operator to a Future, you don't have a future anymore. If you apply map to some Future<V, E>, you get a Map<Future<V, E>, V2>, and similar for every other operator. The types quickly get out of hand and obscure the fact that there's a Future at the bottom.

If you really want to, you can implement your own operator to convert any Publisher to a Future. But you'll have to decide what to do if the upstream emits .finished, since a Future cannot emit .finished.

extension Publisher {
    func asFuture() -> Future<Output, Failure> {
        return Future { promise in
            var ticket: AnyCancellable? = nil
            ticket = self.sink(
                receiveCompletion: {
                    ticket?.cancel()
                    ticket = nil
                    switch $0 {
                    case .failure(let error):
                        promise(.failure(error))
                    case .finished:
                        // WHAT DO WE DO HERE???
                        fatalError()
                    }
            },
                receiveValue: {
                    ticket?.cancel()
                    ticket = nil
                    promise(.success($0))
            })
        }
    }
}
like image 145
rob mayoff Avatar answered Nov 11 '22 15:11

rob mayoff


Instead of converting a data task publisher into a Future, convert a data task into a Future. Just wrap a Future around a call to a URLSession's dataTask(...){...}.resume() and the problem is solved. This is exactly what a future is for: to turn any asynchronous operation into a publisher.

like image 3
matt Avatar answered Nov 11 '22 15:11

matt