Convert URLSession.DataTaskPublisher to Future publisher - ios

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?

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

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.

How to return a future from a function
Instead of trying to return a 'future' from a function you need to convert your existing publisher to a AnyPublisher<Value, Error>. You do this by using the .eraseToAnyPublisher() operator.
func getUser() -> AnyPublisher<User, Error> {
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { output -> Data in
// handle response
return output.data
}
.decode(type: User.self, decoder: JSONDecoder())
.mapError { error in
// handle error
return error
}
.eraseToAnyPublisher()
}

Related

iOS Combine Start new request only if previous has finished

I have network request that triggers every last cell in switui appearas. Sometimes if user scrolls fast enough down -> up -> request will trigger before first one finishes. Without combine or reactive approach I have completion block and bool value to handle this:
public func load() {
guard !isLoadingPosts else { return }
isLoadingPosts = true
postsDataProvider.loadMorePosts { _ in
self.isLoadingPosts = false
}
}
I was wondering if with combine this can be resolved more elegantly, without the need to use bool value. For example execute request only if previous has finished?
It looks like you want to skip making the call if it's already in progress.
Since you didn't share any of the Combine code you might have, I'll assume that you have a publisher-returning function like this:
func loadMorePosts() -> AnyPublisher<[Post], Error> {
//...
}
Then you can use a subject to initiate a load call, a flatMap(maxPublishers:_:) downstream, with a number of publishers limited to 1:
let loadSubject = PassthroughSubject<Void, Never>()
loadSubject
.flatMap(maxPublishers: .max(1)) {
loadMorePosts()
}
.sink(
receiveCompletion: { _ in },
receiveValue: { posts in
// update posts
})
.store(in: &cancellables)
The above pipeline subscribes to the subject, but if another value arrives before flatMap is ready to receive it, it would simply be dropped.
Then the load function becomes:
func load() {
loadSubject.send(())
}

Rx: How to modify a shared source observable within a retry

Top Level Question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
Solution(s) I have considered:
The suggestion of using defer from this post doesn't seem to naturally port over if the source observable needs to be shared.
Use case (to fully elaborate the question)
Say I have a server connection object that, when initialized, connects to an url. Once it is created, I can also use it to get a data stream for a particular input.
class ServerConnection {
var url: URL
init(url: URL)
func getDataStream(input: String) -> Observable<Data> // the observable also errors when the instance is destroyed.
}
However, one particular url or another may be broken or overloaded. So I may want to obtain the address of a mirror and generate a new ServerConnection object. Let's say I have such a function.
// At any point in time, gets the mirror of the url with the lowest load
func getLowestLoadMirror(url: URL) -> URL {}
Ideally, I want this "mirror url" switching should be an implementation detail. The user of my code may only care about the data they receive. So we would want to encapsulate this logic in a new class:
class ServerConnectionWithMirrors {
private var currentConnection: BehaviorRelay<ServerConnection>
init(startingURL: URL)
func dataStream(for inputParams: String) -> Observable<Data>
}
// usage
let connection = ServerConnectionWithMirrors(startingURL: "www.example.com")
connection.dataStream(for: "channel1")
.subscribe { channel1Data in
// do something with channel1Data
}.disposed(by: disposeBag)
connection.dataStream(for: "channel2")
.subscribe { channel2Data in
// do something with channel2Data
}.disposed(by: disposeBag)
How should I write the dataStream() function for ServerConnectionWithMirrors? I should be using retries, but I need to ensure that the retries, when faced with a particular error (ServerOverLoadedError) update the value on the behaviorRelay.
Here is code that I have so far that demonstrates the crux at what I am trying to do. One problem is that multiple subscribers to the behaviorRelay may all update it in rapid succession when they get an error, where only one update would do.
func dataStream(for inputParams: String) -> Observable<Data> {
self.currentConnection.asObservable()
.flatMapLatest { server in
return server.getDataStream(input: inputParams)
}
.retryWhen { errors in
errors.flatMapLatest { error in
if error is ServerOverLoadedError {
self.currentConnection.accept(ServerConnection(url: getLowestLoadURL()))
} else {
return Observable.error(error)
}
}
}
}
The answer to your top level question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
You cannot modify a retry's source observable from within the retry. (full stop) You cannot do this whether it is shared or not. What you can do is make the source observable in such a way that it naturally updates its data for every subscription.
That is what the question you referred to is trying to explain.
func getData(from initialRequest: URLRequest) -> Observable<Data> {
return Observable.deferred {
var correctRequest = initialRequest
let correctURL = getLowestLoadMirror(url: initialRequest.url!)
correctRequest.url = correctURL
return Observable.just(correctRequest)
}
.flatMapLatest {
getDataFromServer(request: $0)
}
.retryWhen { error in
error
.do(onNext: {
guard $0 is ServerOverloadedError else { throw $0 }
})
}
}
With the above code, every time deferred is retried, it will call its closure and every time its closure is called, the URL will the lowest load will be used.

How to flatMap two Publishers with different Failure types in Combine

I follow a pattern in my Rx code, I usually have an Observable trigger which I flatMap to create another Observable for a network request. A simplified example:
enum ViewModelError: Error {
case bang
}
enum DataTaskError: Error {
case bang
}
func viewModel(trigger: Observable<Void>,
dataTask: Observable<Result<SomeType, DataTaskError>>) -> Observable<Result<AnotherType, ViewModelError>> {
let apiResponse = trigger
.flatMap { dataTask }
}
The Combine equivalent I'm having some trouble with. I could use a Result as the Output type and use Never as the Failure type but that feels like a misuse of the API.
func viewModel(trigger: AnyPublisher<Void, Never>,
dataTask: AnyPublisher<SomeType, DataTaskError>) -> AnyPublisher<AnotherType, ViewModelError> {
let apiResponse = trigger
.flatMap { dataTask }
}
I get a compilation error:
Instance method 'flatMap(maxPublishers:_:)' requires the types 'Never' and 'DataTaskError' be equivalent
I could use mapError and cast both of the errors to Error, but I need a DataTaskError to be able to create my ViewModelError.
This feels like it shouldn't be so difficult, and it seems like a fairly common use case. I'm likely just misunderstanding some fundamentals, a point in the right direction would be greatly appreciated.
When you have a publisher with Never as failure type, you can use setFailureType(to:) to match the failure type of another publisher. Note that this method can only be used when the failure type is Never, according to the doc. When you have an actual failure type you can convert the error with mapError(_:). So you can do something like this:
func viewModel(trigger: AnyPublisher<Void, Never>,
dataTask: AnyPublisher<SomeType, DataTaskError>) -> AnyPublisher<AnotherType, ViewModelError> {
trigger
.setFailureType(to: ViewModelError.self) // Publisher<Void, ViewModelError>
.flatMap {
dataTask // Publisher<SomeType, DataTaskError>
.mapError { _ in ViewModelError.bang } // Publisher<SomeType, ViewModelError>
.map { _ in AnotherType() } // Publisher<AnotherType, ViewModelError>
}
.eraseToAnyPublisher()
}

Combine turn one Publisher into another

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?
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)

How to schedule a synchronous sequence of asynchronous calls in Combine?

I'd like to handle a series of network calls in my app. Each call is asynchronous and flatMap() seems like the right call. However, flatMap processes all arguments at the same time and I need the calls to be sequential -- the next network call starts only after the previous one is finished. I looked up an RxSwift answer but it requires concatMap operator that Combine does not have. Here is rough outline of what I'm trying to do, but flatMap fires all myCalls at the same time.
Publishers.Sequence(sequence: urls)
.flatMap { url in
Publishers.Future<Result, Error> { callback in
myCall { data, error in
if let data = data {
callback(.success(data))
} else if let error = error {
callback(.failure(error))
}
}
}
}
After experimenting for a while in a playground, I believe I found a solution, but if you have a better idea, please share. The solution is to add maxPublishers parameter to flatMap and set the value to max(1)
Publishers.Sequence(sequence: urls)
.flatMap(maxPublishers: .max(1)) // <<<<--- here
{ url in
Publishers.Future<Result, Error> { callback in
myCall { data, error in
if let data = data {
callback(.success(data))
} else if let error = error {
callback(.failure(error))
}
}
}
}
You can also use prepend(_:) method on observable which creates concatenated sequence which, I suppose is similar to Observable.concat(:) in RxSwift.
Here is a simple example that I tried to simulate your use case, where I have few different sequences which are followed by one another.
func dataTaskPublisher(_ urlString: String) -> AnyPublisher<(data: Data, response: URLResponse), Never> {
let interceptedError = (Data(), URLResponse())
return Publishers.Just(URL(string: urlString)!)
.flatMap {
URLSession.shared
.dataTaskPublisher(for: $0)
.replaceError(with: interceptedError)
}
.eraseToAnyPublisher()
}
let publisher: AnyPublisher<(data: Data, response: URLResponse), Never> = Publishers.Empty().eraseToAnyPublisher()
for urlString in [
"http://ipv4.download.thinkbroadband.com/1MB.zip",
"http://ipv4.download.thinkbroadband.com/50MB.zip",
"http://ipv4.download.thinkbroadband.com/10MB.zip"
] {
publisher = publisher.prepend(dataTaskPublisher(urlString)).eraseToAnyPublisher()
}
publisher.sink(receiveCompletion: { completion in
print("Completed")
}) { response in
print("Data: \(response)")
}
Here, prepend(_:) operator prefixes the sequence and so, prepended sequences starts first, completes and next sequence start.
If you run the code below, you should see that firstly 10 MB file is download, then 50 MB and at last 1 MB, since the last prepended starts first and so on.
There is other variant of prepend(_:) operator which takes array, but that does not seem to work sequentially.

Resources