How can I get a detailed array from another observable of array - swift

I am beginner in RxSwift in general trying to chain two different operation with my REST API to get all detailed products.
I wrapped some REST API to return RxSwift Observable, one return a list of product and other the product detail.
class API {
func listProduct() -> Observable<[Product]> { ... }
func detailProdcut(code: Int) -> Observable<[ProductDetail]> { ... }
}
Now I want to get the product detail from a product list, how can I do this in Rx way ? I'm trying to do something like
API.init().listProduct()
.flapMap { products -> <Product> in return products[0] }
.map { product in API.init(product.code) }
.merge()
.toArray
But isn't work, and I very confused about how to transform one list of products code into an array of products detail

let api = API()
let productDetails = api.listProducts()
.flatMap { products in
let productsObservable = Observable.from(products)
let productDetails = productsObservable.flatMap { api.detail(product($0.code) }
return productDetails.toArray()
}
What is going on here :
flatMap has specialized type [Product] -> Observable<[ProductDetails]>. How? Read on.
Observable.from takes a swift array and transforms it into an observable. It will emit next event for each of the element of the array. So we now have Observable<Product> in productsObservable.
the call to flatMap on productObservable will create an Observable<ProductDetail> for each next event it sends. We now have an observable that will send as many next event as their are products in the result of listProducts.
We use toArray to transform this observable to something that will emit only one next event, an array aggregating all the results from productDetails.

Related

Convert '[Publishers.Map<URLSession.DataTaskPublisher, [ProductRep]?>]' to [ProductRep]

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.

Observable being disposed ahead of time

I think it's better if I explain what I'm trying to achieve because I think the error is on my misunderstanding on how Observables work.
I have a UIViewController that contains a UITableView I'm also using RxSwift and RxDataSources, so I'm binding my tableView items like this:
vm.model
.debug()
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
Where vm is a viewModel which contains:
self.model = self.network.provider.getMarkets()
.map { (markets: [Market]) -> [Row] in
var rows = [Row]()
for market in markets {
rows.append(.market(market: market))
}
return rows
}
.map { (rows: [Row]) -> [Model] in
return [Model(section: .market, items: rows)]
}
.shareReplay(1)
.asDriver(onErrorJustReturn: [])
Where model is:
var model: Driver<[Model]>
This all works great the first time, the tableview displays the items, but the print from the debug():
2017-04-28 20:07:21.382: MarketAndLanguageSelectionViewController.swift:36 (viewDidLoad()) -> subscribed
2017-04-28 20:07:22.287: MarketAndLanguageSelectionViewController.swift:36 (viewDidLoad()) -> Event next(*Multiple items*)
2017-04-28 20:07:22.289: MarketAndLanguageSelectionViewController.swift:36 (viewDidLoad()) -> Event completed
2017-04-28 20:07:22.289: MarketAndLanguageSelectionViewController.swift:36 (viewDidLoad()) -> isDisposed
The problem is I didn't want the datasource to dispose because I wan't to update it based on the user action. If the user clicks a tableViewCell I want to update the model. Any ideas on how can I achieve this?
Sorry for such a big question.
I'm guessing that network.provider.getMarkets() makes a network call which returns a single result and completes.
Now, getMarkets() is the source, and tableView.rx.items is the sink. Once the source completes, the chain is broken.
It sounds like what you want to do is create a new getMarkets Observable every time the user taps something, as well as calling getMarkets once for free. I would expect something like:
let markets = trigger.flatMap {
self.network.provider.getMarkets()
}.map { (markets: [Market]) -> [Row] in
var rows = [Row]()
for market in markets {
rows.append(.market(market: market))
}
return rows
}.map { (rows: [Row]) -> [Model] in
return [Model(section: .market, items: rows)]
}.startWith(self.network.provider.getMarkets())
.shareReplay(1)
.asDriver(onErrorJustReturn: [])
Note that the only real difference is the beginning trigger.flatMap {. Your source will then be the button or whatever the user taps on to cause the network update which won't complete until it's deleted.
(The above is untested code, but it should give you an idea of the shape you want.)

rxswift: how to get the Observable<Any> resulting in a combination of Observable<[Category]>?

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

RxSwift: Observe rx_text & rx_offset simultaneously

I've been working on a search controller that is using RxSwift to update DataSource when user types in Search field, like it's described here: http://www.thedroidsonroids.com/blog/ios/rxswift-examples-3-networking/
That is my viewmodel:
struct SearchControllerViewModel {
let provider: RxMoyaProvider<ThreadifyEndpoint>
let startsWith: Observable<String>
func startSearching() -> Observable<[SearchedNodeViewModel]> {
return startsWith
.observeOn(MainScheduler.instance)
.flatMapLatest { query -> Observable<[SearchedNodeViewModel]?> in
return self.findNodes(query)
}.replaceNilWith([])
}
internal func findNodes(startsWith: String) -> Observable<[SearchedNodeViewModel]?> {
return self.provider
.request(ThreadifyEndpoint.SearchForNodes(startsWith: startsWith))
.mapArrayOptional(SearchedNodeViewModel.self)
}
}
Now I want new data to be loaded not only when user is typing but either when sh's scrolling down.
I was thinking to use combineLatest to observe both rx_text and rx_offset but I can't pass Observable to combineLatest because of compilation error.
The compilation error you're seeing is due to you not using an actual method. The method signature you're intending to use is:
public class func combineLatest<O1 : ObservableType, O2 : ObservableType>(source1: O1, _ source2: O2, resultSelector: (O1.E, O2.E) throws -> Element) -> RxSwift.Observable<Element>
Notice that there's a third argument that you're forgetting: resultSelector. That's supposed to be a block that describes how you want to combine the latest elements into a new element.
Based on your error message, I'm thinking you're using it like this:
let combined = Observable.combineLatest(stringObservable, pointObservable)
Whereas, you should be using it like this:
let combined = Observable.combineLatest(stringObservable, pointObservable) { (s, p) in
return "\(s), \(p)" // or construct your new element however you'd like
}
Without the block, RxSwift doesn't know how you'd like to combine them. You might have been thinking it would just default to making a new tuple of (String, CGPoint) as the element, but it makes no such assumptions and requires you to tell it.

RxSwift: chaining several actions

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.