Swift Combine - Merging multiple Futures - ios

I'm trying to merge multiple Futures with combine and I'm out of luck so far. I'm trying using Publishers.MergeMany similar to this.
let f = Future<Void, Error> { promise in
promise(.success(Void()))
}
let g = Future<Void, Error> { promise in
promise(.success(Void()))
}
var subscriptions = Set<AnyCancellable>()
Publishers.MergeMany([f,g]).collect()
.sink { _ in
print("merge")
} receiveValue: { value in
print("done")
}.store(in: &subscriptions)
For some reason on my real case scenario, only the first publisher finish but the other one get stuck in oblivion.
If a call each Future one by one sequentially, all works fine.
Does anybody had an issue like this before?

Related

How to wait for the first sink to get called before running a snapshot test [duplicate]

I'm struggling with unit testing of Published object.
I have a viewmodel class as below
class MovieListViewModel {
#Published public private(set) var arrayOfMovies: [Movie] = []
#Published private var arraofFavoriteMoviesID: [FavouriteMovieID] = []
init(request: NetworkServiceProtocol) {
addSubscribers()
callServicesInSequence()
}
func addSubscribers() {
$arrayOfMovies.combineLatest($arraofFavoriteMoviesID)
.debounce(for: 0.0, scheduler: DispatchQueue.main)
.sink { [weak self] (_, _) in
self?.fetchWachedMovies()
self?.fetchTobeWatchedMovies()
self?.fetchFavoriteMovies()
}
.store(in: &subscriptions)
}
func callServicesInSequence() {/*..service request...*}
}
Here addSubscribers() listen for any changes happening in arrayOfMovies or arraofFavoriteMoviesID and works perfectly is the app.
But when I tried to mock and write unit test cases. Any chnages happening in arrayOfMovies or arraofFavoriteMoviesID does not make any effect (addSubscribers's body never get called).
Can any one please guide me what am I doing wrong while writing unit test cases for Combine/Published objects.
Please let me know if more clarification required.
Your code has two obvious dependencies: a NetworkServiceProtocol and DispatchQueue.main.
NetworkServiceProtocol is not part of the iOS SDK. I assume it is a type you created, and you pass it to the model's init, so you can substitute a testable implementation in your test cases.
However, DispatchQueue is part of the iOS SDK, and you cannot create your own testable implementation of it for use in test cases. You have only a limited ability to run the main queue, which makes it difficult to test code that depends on it.
Here are three solutions:
My favorite solution is to adopt The Composable Architecture (TCA) or a similar framework, which by design makes it easy to control dependencies and hence to test code like this.
A less invasive solution is to replace the direct use of DispatchQueue.main with a type eraser, which you pass to the model's init. Then, in tests, you can pass in a deterministic scheduler that you control in the test case. The Combine Schedulers package, for example, provides the type eraser AnyScheduler and several scheduler implementations specifically for use in testing. (TCA, mentioned above, uses this package.)
Writing your own type eraser for the Scheduler protocol is simple enough that you could do it yourself if you don't want to depend on a third-party package.
The least invasive solution is to use XCTestExpectation APIs to run the main queue in your test case. I'll demonstrate that here.
You didn't post enough code to demonstrate, so I'll use the following simple types:
struct Movie: Equatable {
let id: UUID = .init()
}
struct NetworkClient {
let fetchMovies: AnyPublisher<[Movie], Error>
}
And here is the simplified model that uses them:
class Model: ObservableObject {
#Published public private(set) var movies: [Movie] = []
#Published public private(set) var error: Error? = nil
private let client: NetworkClient
private var fetchTicket: AnyCancellable? = nil
init(client: NetworkClient) {
self.client = client
fetchMovies()
}
func fetchMovies() {
fetchTicket = client.fetchMovies
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] in
self?.fetchTicket = nil
if case .failure(let error) = $0 {
self?.error = error
}
},
receiveValue: { [weak self] in
self?.movies = $0
}
)
}
}
For testing, I can set up a NetworkClient where fetchMovies is a PassthroughSubject. That way my test case can decide exactly what the “network” sends and when it sends it.
To test the success case, where the network “works”, I subscribe to the model's $movies publisher and fulfill an XCTestExpectation if it publishes the correct value.
func testSuccess() throws {
let fetchMovies = PassthroughSubject<[Movie], Error>()
let client = NetworkClient(
fetchMovies: fetchMovies.eraseToAnyPublisher()
)
let model = Model(client: client)
let expectedMovies = [ Movie(), Movie() ]
let ex = expectation(description: "movies publishes correct value")
let ticket = model.$movies.sink { actualMovies in
if actualMovies == expectedMovies {
ex.fulfill()
}
}
fetchMovies.send(expectedMovies)
waitForExpectations(timeout: 2)
// Some mention of ticket here keeps the subscription alive
// during the wait for the expectation.
ticket.cancel()
}
To test the failure case, where the network “fails”, I subscribe to the $error publisher and fulfill an XCTestExpectation if it publishes the correct error code.
func testFailure() throws {
let fetchMovies = PassthroughSubject<[Movie], Error>()
let client = NetworkClient(
fetchMovies: fetchMovies.eraseToAnyPublisher()
)
let model = Model(client: client)
let expectedCode = URLError.Code.resourceUnavailable
let ex = expectation(description: "error publishes correct code")
let ticket = model.$error.sink { error in
if (error as? URLError)?.code == expectedCode {
ex.fulfill()
}
}
fetchMovies.send(completion: .failure(URLError(expectedCode)))
waitForExpectations(timeout: 2)
ticket.cancel()
}
Note though that if a test fails (for example if you change testFailure to purposely publish the wrong code), it takes 2 seconds to fail. That is annoying. These two tests are simple enough that we could rewrite them to fail quicker in the case that the wrong thing is published. But in general it might be difficult to write all test cases to “fail fast” when relying on XCTestExpectation. That is the sort of problem you can avoid by replacing the direct use of DispatchQueue with a type eraser. It lets your test case use a controllable scheduler so the test case can make time flow instantly, without any use of DispatchQueues, so you don't need to use XCTestExpectation at all.

CombineLatest operator is not emitting when inners publishers use subscribe(on:)

I'm observing an unexpected behavior regarding CombineLatest, if the inner publishers has subscribe(on:), the CombineLatest stream is not emitting any value.
Notes:
With Zip operator is working
Moving the subscribe(on:) / receive(on:) to the combineLatest stream also work. But in this particular use case, the inner publishers is defining their subscribe/receive
because are (re)used in other places.
Adding subscribe(on:)/receive(on:) to only one of the inner publishers also work, so the problem is just when both have it.
func makePublisher() -> AnyPublisher<Int, Never> {
Deferred {
Future { promise in
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 3) {
promise(.success(Int.random(in: 0...3)))
}
}
}
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
var cancellables = Set<AnyCancellable>()
Publishers.CombineLatest(
makePublisher(),
makePublisher()
)
.sink { completion in
print(completion)
} receiveValue: { (a, b) in
print(a, b)
}.store(in: &cancellables)
Is this a combine bug or expected behavior? Do you have any idea of how can be setup this kind of stream where the inners can define its own subscribe scheduler?
Yes, it's a bug. We can simplify the test case to this:
import Combine
import Dispatch
let pub = Just("x")
.subscribe(on: DispatchQueue.main)
let ticket = pub.combineLatest(pub)
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) })
This never prints anything. But if you comment out the subscribe(on:) operator, it prints what's expected. If you leave subscribe(on:) in, but insert some print() operators, you'll see that the CombineLatest operator never sends any demand upstream.
I suggest you copy the CombineX reimplementation of CombineLatest and the utilities it needs to compile (the CombineX implementations of Lock and LockedAtomic, I think). I don't know that the CombineX version works either, but if it's buggy, at least you have the source and can try to fix it.

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

How can I branch out multiple API calls from the result of one API call and collect them after all are finished with Combine?

So, I have this sequence of API calls, where I fetch a employee details, then fetch the company and project details that the employee is associated with. After both fetching are complete, I combine both and publish a fetchCompleted event. I've isolated the relevant code below.
func getUserDetails() -> AnyPublisher<UserDetails, Error>
func getCompanyDetails(user: UserDetails) -> AnyPublisher<CompanyDetails, Error>
func getProjectDetails(user: UserDetails) -> AnyPublisher<ProjectDetails, Error>
If I do this,
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Never> {
let cvs = CurrentValueSubject<UserFetchState, Error>(.initial)
let companyPublisher = getUserDetails()
.flatMap { getCompanyDetails($0) }
let projectPublisher = getUserDetails()
.flatMap { getProjectDetails($0) }
companyPublisher.combineLatest(projectPublisher)
.sink { cvs.send(.fetchComplete) }
return cvs.eraseToAnyPublisher()
}
getUserDetails() will get called twice. What I need is fetch the userDetails once and with that, branch the stream into two, map it to fetch the company details and project details and re-combine both.
Is there a elegant(flatter) way to do the following.
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Never> {
let cvs = CurrentValueSubject<UserFetchState, Error>(.initial)
getUserDetails()
.sink {
let companyPublisher = getCompanyDetails($0)
let projectPublisher = getProjectDetails($0)
companyPublisher.combineLatest(projectPublisher)
.sink { cvs.send(.fetchComplete) }
}
return cvs.eraseToAnyPublisher()
}
The whole idea of Combine is that you construct a pipeline down which data flows. Actually what flows down can be a value or a completion, where a completion could be a failure (error). So:
You do not need to make a signal that the pipeline has produced its value; the arrival of that value at the end of the pipeline is that signal.
Similarly, you do not need to make a signal that the pipeline's work has completed; a publisher that has produced all the values it is going to produce produces the completion signal automatically, so the arrival of that completion at the end of the pipeline is that signal.
After all, when you receive a letter, the post office doesn't call you up on the phone and say, "You've got mail." Rather, the postman hands you the letter. You don't need to be told you've received a letter; you simply receive it.
Okay, let's demonstrate. The key to understanding your own pipeline is simply to track what kind of value is traveling down it at any given juncture. So let's construct a model pipeline that does the sort of thing you need done. I will posit three types of value:
struct User {
}
struct Project {
}
struct Company {
}
And I will imagine that it is possible to go online and fetch all of that information: the User independently, and the Project and Company based on information contained in the User. I will simulate that by providing utility functions that return publishers for each type of information; in real life these would probably be deferred futures, but I will simply use Just to keep things simple:
func makeUserFetcherPublisher() -> AnyPublisher<User,Error> {
Just(User()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func makeProjectFetcherPublisher(user:User) -> AnyPublisher<Project,Error> {
Just(Project()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func makeCompanyFetcherPublisher(user:User) -> AnyPublisher<Company,Error> {
Just(Company()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
Now then, let's construct our pipeline. I take it that our goal is to produce, as the final value in the pipeline, all the information we have collected: the User, the Project, and the Company. So our final output will be a tuple of those three things. (Tuples are important when you are doing Combine stuff. Passing a tuple down the pipeline is extremely common.)
Okay, let's get started. In the beginning there is nothing, so we need an initial publisher to kick off the process. That will be our user fetcher:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
What's coming out the end of that pipeline is a User. We now want to feed that User into the next two publishers, fetching the corresponding Project and Company. The way to insert a publisher into the middle of a pipeline is with flatMap. And remember, our goal is to produce the tuple of all our info. So:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
// at this point, the value is a User
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
// ?
}
// at this point, the value is a tuple: (User,Project,Company)
So what goes into flatMap, where the question mark is? Well, we must produce a publisher that produces the tuple we have promised. The tuple-making publisher par excellence is Zip. We have three values in our tuple, so this is a Zip3:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
// ?
let result = Publishers.Zip3(/* ? */)
return result.eraseToAnyPublisher()
}
So what are we zipping? We must zip publishers. Well, we know two of those publishers — they are the publishers we have already defined!
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
let pub1 = self.makeProjectFetcherPublisher(user: user)
let pub2 = self.makeCompanyFetcherPublisher(user: user)
// ?
let result = Publishers.Zip3(/* ? */, pub1, pub2)
return result.eraseToAnyPublisher()
}
We're almost done! What goes in the missing slot? Remember, it must be a publisher. And what's our goal? We want to pass on the very same User that arrived from upstream. And what's the publisher that does that? It's Just! So:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
let pub1 = self.makeProjectFetcherPublisher(user: user)
let pub2 = self.makeCompanyFetcherPublisher(user: user)
let just = Just(user).setFailureType(to:Error.self)
let result = Publishers.Zip3(just, pub1, pub2)
return result.eraseToAnyPublisher()
}
And we're done. No muss no fuss. This is a pipeline that produces a (User,Project,Company) tuple. Whoever subscribes to this pipeline does not need some extra signal; the arrival of the tuple is the signal. And now the subscriber can do something with that info. Let's create the subscriber:
myWonderfulPipeline.sink {
completion in
if case .failure(let error) = completion {
print("error:", error)
}
} receiveValue: {
user, project, company in
print(user, project, company)
}.store(in: &self.storage)
We didn't do anything very interesting — we simply printed the tuple contents. But you see, in real life the subscriber would now do something useful with that data.
You can use the zip operator to get a Publisher which emits a value whenever both of its upstreams emitted a value and hence zip together getCompanyDetails and getProjectDetails.
You also don't need a Subject to signal the fetch being finished, you can just call map on the flatMap.
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Error> {
getUserDetails()
.flatMap { getCompanyDetails(user: $0).zip(getProjectDetails(user: $0)) }
.map { _ in UserFetchState.fetchComplete }
.eraseToAnyPublisher()
}
However, you shouldn't need a UserFetchState to signal the state of your pipeline (and especially shouldn't throw away the fetched CompanyDetails and ProjectDetails objects in the middle of your pipeline. You should simply return the fetched CompanyDetails and ProjectDetails as a result of your flatMap.
func getCompleteUserDetails() -> AnyPublisher<(CompanyDetails, ProjectDetails), Error> {
getUserDetails()
.flatMap { getCompanyDetails(user: $0).zip(getProjectDetails(user: $0)) }
.eraseToAnyPublisher()
}

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

Resources