How do I figure out the flatMap, reduce, etc. to turn this into an array of ProductRep?
I'm currently getting Cannot convert value of type 'Publishers.Map<URLSession.DataTaskPublisher, [StoreService.ProductRep]?>' to closure result type 'StoreService.ProductRep' - because I don't really understand how to turn a Publishers.Map into the actual thing.
This is intended to be part of a larger network of publishers and subscribers using map/flatMap.
let mapResult: [ProductRep] = parsePageURIs(firstPage, jsonDict)
.map { pageUri in self.importProductsPublisher(pageUri, urlSession: urlSession, firstPage: false) }
.map { publisher in publisher.map {(productsData, productsResponse) in
return self.handleImportProductsResponse(data: productsData, response: productsResponse, category: category, urlSession: urlSession)
}
}
func importProductsPublisher(_ uri: String, urlSession: URLSession, firstPage: Bool) -> URLSession.DataTaskPublisher { /* Start the network query */ }
func handleImportProductsResponse(data: Data, response: URLResponse, category: CategoryRep, urlSession: URLSession) -> [ProductRep]? { /* Handle the result of the network query and parse into an array of items (ProductRep) */ }
I think there's 2 things going on here. Firstly, you are missing a sink of some kind, usually you'll see a chain of publishers end in something like .sink or .assign
whatever.map{ ... }.sink{ items in
//Do something with items in this closure
}
Secondly is you appear to be mapping page uris to a collection of Publishers, rather than a single publisher. Then you have a nested map inside a map, which doesn't resolve anything yet.
You may be able to use one of the merge publishers, like merge many and collect to reduce from one publisher to many:
https://developer.apple.com/documentation/combine/publishers/mergemany/3209693-collect
A basic collection example:
let arrayOfPublishers = (0...10).map{ int in
return Just(int)
}
Publishers.MergeMany(arrayOfPublishers).collect().sink { allTheInts in
//allTheInts is of type [Int]
print(allTheInts)
}
I think you need something like this:
let productPublishers = parsePageURIs(firstPage, jsonDict).map { pageUri in
return self.importProductsPublisher(pageUri, urlSession: urlSession, firstPage: false).map {(productsData, productsResponse) in
return self.handleImportProductsResponse(data: productsData, response: productsResponse, category: category, urlSession: urlSession)
}
}
Publishers.MergeMany(productPublishers).collect().sink { products in
//Do somethings with the array of products
}
map the uris to publisher results in a Model after mapping a data request publisher, then merge all your publishers to take the individual results into one array with MergeMany & collect, and finally the sink which actually triggers everything to happen.
Related
I am a RxSwift beginner and making a app with RxSwift + MVVM.
I have a method which calls API and converts to RxCocoa.Driver in ViewModel class like below.
func fetch() -> Driver<HomeViewEntity> {
apiUseCase.fetch(query: HomeViewQuery())
.map { data in
HomeViewEntity(userName: data.name,
emailAddress: data.email
}
.asDriver(onErrorRecover: { [weak self] error in
if let printableError = error as? PrintableError {
self?.errorMessageRelay.accept(AlertPayload(title: printableError.title, message: printableError.message))
}
return Driver.never()
})
}
Now, I'd like to call this fetchListPlace() method at regular intervals a.k.a polling (e.g. each 5 minutes) at ViewController.
How to do that????
I think interval is suit in this case, but I can't get an implementation image....
Here you go:
func example(_ fetcher: Fetcher) -> Driver<HomeViewEntity> {
Driver<Int>.interval(.seconds(5 * 60))
.flatMap { _ in fetcher.fetch() }
}
Also note,
Returning a Driver.never() from your recovery closure is probably a bad idea. Prefer Driver.empty() instead.
I'm not a fan of putting a side effect in the recovery closure in the first place. I think it would be better to have the fetch() return a Driver<Result<HomeViewEntity, Error>> instead and move the side effect to the end of the chain (in a subscribe or a flatMap.)
I perform many repeated requests in order to populate a field. I would like to cache the result and use the cached value the next time around.
public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
if let item = itemCache[id] {
return Just(item).eraseToAnyPublisher()
}
return downloadItem(id: id)
.map { item in
if let item = item {
itemCache[id] = item
}
return item
}
.eraseToAnyPublisher()
}
}
func downloadItem(_ id: String) -> AnyPublisher<Item?, Never> { ... }
And this is called like this:
Just(["a", "a", "a"]).map(getItem)
However, all the requests are calling downloadItem. downloadItem does return on the main queue. I also tried wrapping the entire getItem function into Deferred but that had the same result.
First, the issue was that the function is being evaluated and only a publisher is returned. So the cache check is evaluated each time before the network publisher is ever subscribed to. Using Deferred is the proper fix for that. However, that still didn't solve the problem.
The solution was instead to first cache a shared publisher while the network request is pending so all requests during the network call will use the same publisher, then when it's complete to cache a Just publisher for the all future calls:
public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
if let publisher = self.publisherCache[id] {
return publisher
}
let publisher = downloadItem(id)
.handleEvents(receiveOutput: {
// Re-cache a Just publisher once the network request finishes
self.publisherCache[id] = Just($0).eraseToAnyPublisher()
})
.share() // Ensure the same publisher is returned from the cache
.eraseToAnyPublisher()
// Cache the publisher to be used while downloading is in progress
self.publisherCache[id] = publisher
return publisher
}
One note, is that downloadItem(id) is async and being recieved on the main loop. When I replaced downloadItem(id) with Just(Item()) for testing, this didn't work beause the entire publisher chain was evaluated on creation. Use Just(Item()).recieve(on: Runloop.main) to fix that while testing.
I have a network request. I'd like to make this request and fallback to a cached value if
the request fails
the request takes too long
I was thinking I could use race to make the request and also use after to give myself a sort of time out on the promise.
Something like -
func getCompany() -> Promise<Company> {
let request: Promise<Company> = client.request(.getCompany)
let cachedResponse: Promise<Company?> = cache.getObject(ofType: Company.self, forKey: "company")
return race(request, after(seconds: 4).then(cachedResponse))
}
This fails to compile however with
Cannot convert value of type 'Promise<Company?>' to expected argument type '(Void) -> Guarantee<_>'
I'd like to say essentially return the first one that resolves, which in this case would be cachedResponse if request took longer than 4 seconds.
There's a couple of things you will need to do first. It looks like one of your requests return an optional, you will need to handle this in some way as race is going to require all promises to conform to the same type.
If you are planning on using this approach elsewhere, I would suggest creating a simple util to wrap around the delayed response.
func await<T>(for delay: TimeInterval, then body: Promise<T>) -> Promise<T> {
return after(seconds: delay).then { _ in body }
}
You can then refactor your getCompany method to race your requests as:
func getCompany() -> Promise<Company?> {
let request: Promise<Company?> = client.request(.getCompany)
let cachedResponse: Promise<Company?> = await(for: 2, then: cache.getObject(ofType: Company.self, forKey: "company"))
return race(request, cachedResponse)
}
I have some doubts in my approach. I have two type of Observables:
//I can fetch from the server the houses and save them in the database
func houses() -> Observable<[House]>
//Given a houseId, I can fetch it's details and save them in the database
func houseDetail(id: Int) -> Observable<Family>
I would like to do an Observable which first fetch all houses, and then fetch the families. What I did is something like that:
//I am an observable which, when I complete, all data are saved
func fetchAllHousesAndFamily() -> Observable<Any> {
var allHousesObservables: [Observable] = []
for house in houses {
allHousesObservables.append(houseDetail(house.id))
}
return Observable.combineLatest(allHousesObservables)
}
But this for...it doesn't seem to be reactive style to me, and it seems like a hack because I don't know enough about rx operators.
Do you have the right way to do it in the rxworld ?
Thank you
To get all family from the result of houses, you will want to use the flatMap operator. flatMap accepts a closure with signature T -> Observable<U>, so, in our specific example, it will be a function of type [House] -> Observable<Family>.
houses().flatMap { houses in
let details: [Observable<Family>] = houses.map { houseDetail($0.id) } // [1]
return Observable.from(details).merge() // [2]
}
[1]: We are mapping an array of house to an array of Observable<Family>.
[2]: from(_:) converts [Observable<Family>] to Observable<Observable<Family>>. merge() then transform it to Observable<Family>
We now have an observable that will emit one next event for each house with its family details.
If we'd prefer to keep the House value around, we could simply map again on the first line, like so:
let details: [Observable<(House, Family)>] = house.map { house in
houseDetail(house.id).map { (house, $0) }
}
Lets imagine we have an array of AnObject instances and need to have following sequence of actions to execute:
send objects to backend via separate calls
after step 1 finishes store this array to DB in batch
after step 2 finishes do additional processing for each item
and we'd want to receive the signal only after all those steps were executed (or there was an error). What is the correct way to achieve this via RxSwift and is it actually possible?
Please find my prototype functions below. Unfortunately I didn't come up with a valid code sample for chaining, so there's nothing to demo.
func makeAPIRequest(object: AnObject) -> Observable<Void> {
...
}
func storeData(data: [AnObject]) -> Observable<Void> {
...
}
func additionalProcessing(object: AnObject) -> Observable<Void> {
...
}
func submitData()
{
let data: [AnObject] = ...;
let apiOperations = data.map{ makeAPIRequest($0) };
let storageOperation = storeData(data);
let processingOperations = data.map{ additionalProcessing($0) };
... // some code to chain steps 1-3
.subscribe { (event) -> Void in
// should be called when operations from step 3 finished
}.addDisposableTo(disposeBag);
}
Let's assume that both makeAPIRequest and additionalProcessing return an Observable<SomeNotVoidType>, and storeData takes an array as its argument and returns an Observable<Array>.
This way, you can do the following:
First, create an array of Observables representing sending individual objects to backend. Then use toObservable method, so the resulting signals can be transformed later on:
let apiOperations = data.map{ makeAPIRequest($0) }.toObservable()
then use merge operator which will make an Observable, that completes only when all of the API calls complete. You can also use toArray operator, which will put the API call results into one array:
let resultsArray = apiOperations.merge().toArray()
This will get you an Observable<Array<ApiResult>>, which will send one Next event when all API operations complete successfully. Now you can store the results in the database:
let storedResults = resultsArray.flatMap { storeDatabase($0) }
Then again you want to make Observables for each array element, so they'll represent additional processing. Note that you need to use flatMap and flatMapLates, otherwise you'll end up with nested observables like Observable<Observable<SomeType>>.
let additionalProcessingResults = storedResults.flatMap {
return $0.map(additionalProcessing).toObservable()
}.flatMapLatest { return $0 }
Then, you can subscribe for successful completion of the additional processing (or you can do something with its individual results):
additionalProcessingResults.subscribe { (event) -> Void in
// should be called when operations from step 3 finished
}.addDisposableTo(disposeBag);
Note that you don't need all the intermediate variables, I just left them to describe all the steps.