iosswiftcombine

Convert URLSession.DataTaskPublisher to Future publisher


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?


Solution

  • 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))
                })
            }
        }
    }