I have a function that fires an API request to the server. I want to loop over it until it returns false (no more data).
func getData(id: Int) -> Observable<Bool> {
return Observable.create { observer in
// Alamofire request
// parse data
// if can decode,
// return true and increment page's property
// otherwise false
// error, if there's a problem
}
}
First try: I've tried using takeWhile, like : getData(id).takeWhile {$0}. It only iterate over my function 1x only.
Second try: using a range. The problem here is that even if my getData function errors out, instead of stopping, the loop continues !
Observable.range(start: 1, count: 100)
.enumerated()
.flatMapLatest({ _ in
self.getData(someID)
})
.subscribe(onNext: { _ in
// save to DB
observer.onNext(true)
observer.onCompleted()
}, onError: { error in
observer.onError(error)
})
.disposed(by: self.disposeBag)
Is there a way to do it, rx style ?
Something like this?
let callApiTrigger = BehaviorRelay<Bool>(value: true)
let callApiEnoughTimes = callApiTrigger.asObservable()
.takeWhile { $0 }
.flatMap { _ in
return getData(someId)
}
.do(onNext: { (apiResult: Bool) in
callApiTrigger.accept(apiResult)
})
The reason why takeWhile and take(X) do not work, is because they do not resubscribe to the Observable. A network request observable typically emits one value at the most.
What you are looking for requires some form of recursion / resubscribing. If you want to it hardcore Rx I suggest you reverse engineer the retry operator to suit your needs. Although I consider myself experienced with RxSwift, that seems like a bridge too far.
Fortunately, I whipped up a recursive approach that works just fine too :)
class PageService {
typealias Page = (pageNumber: Int, items: [String]?)
private func getPage(_ pageNumber: Int) -> Observable<Page> {
let pageToReturn = Page(pageNumber: pageNumber, items: (pageNumber < 3) ? ["bert, henk"] : nil)
return Observable<Page>
.just(pageToReturn)
.delay(0.5, scheduler: MainScheduler.instance)
}
func allPagesFollowing(pageNumber: Int) -> Observable<Page> {
let objectToReturnInCaseOfError = Page(pageNumber: pageNumber + 1, items: nil)
return getPage(pageNumber + 1)
// in order to error out gracefully, you could catch the network error and interpret it as 0 results
.catchErrorJustReturn(objectToReturnInCaseOfError)
.flatMap { page -> Observable<Page> in
// if this page has no items, do not continue the recursion
guard page.items != nil else { return .empty() }
// glue this single page together with all the following pages
return Observable<Page>.just(page)
.concat(self.allPagesFollowing(pageNumber: page.pageNumber))
}
}
}
_ = PageService().allPagesFollowing(pageNumber: 0)
.debug("get page")
.subscribe()
This will print:
2018-03-30 11:56:24.707: get page -> subscribed
2018-03-30 11:56:25.215: get page -> Event next((pageNumber: 1, data: Optional(["bert, henk"])))
2018-03-30 11:56:25.718: get page -> Event next((pageNumber: 2, data: Optional(["bert, henk"])))
2018-03-30 11:56:26.223: get page -> Event completed
2018-03-30 11:56:26.223: get page -> isDisposed
Related
In a personal project of mine, I have created an API caller to retrieve a user's saved tracks from the Spotify API. The Spotify endpoint which I am using has a limit (maximum of 50 tracks per request) as well as an offset (starting index of first track in request), which is why I decided to use a FOR loop to get a series of track pages (each 50 tracks) and append them to a global array. The data is loaded from the main thread, and while the data is being requested, I display a child view controller with a spinner view. Once the data request has completed, I remove the spinner view, and transition to another view controller (passing the data as a property).
I have tried many things, but the array of tracks is always empty following the API request. I have a feeling it has to do with the synchronicity of my request, or maybe its possible that I'm not handling it correctly. Ideally, I would like to wait until the request from my API finishes, then append the result to the array. Do you have any suggestions on how I could solve this? Any help is much appreciated!
func createSpinnerView() {
let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)
DispatchQueue.main.async { [weak self] in
if (self?.dropdownButton.dropdownLabel.text == "My saved music") {
self?.fetchSavedMusic() { tracksArray in
self?.tracksArray = tracksArray
}
}
...
self?.remove(asChildViewController: loadViewController)
self?.navigateToFilterScreen(tracksArray: self!.tracksArray)
}
}
private func fetchSavedMusic(completion: #escaping ([Tracks]) -> ()) {
let limit = 50
var offset = 0
var total = 200
for _ in stride(from: 0, to: total, by: limit) {
getSavedTracks(limit: limit, offset: offset) { tracks in
//total = tracks.total
self.tracksArray.append(tracks)
}
print(offset, limit)
offset = offset + 50
}
completion(tracksArray)
}
private func getSavedTracks(limit: Int, offset: Int, completion: #escaping (Tracks) -> ()) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { (result) in
switch result {
case .success(let model):
completion(model)
print("success")
case .failure(let error):
print("Error retrieving saved tracks: \(error.localizedDescription)")
print(error)
}
}
}
private func navigateToFilterScreen(tracksArray: [Tracks]) {
let vc = FilterViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
vc.paginatedTracks = tracksArray
show(vc, sender: self)
}
First you need to call completion when all of your data is loaded. In your case you call completion(tracksArray) before any of the getSavedTracks return.
For this part I suggest you to recursively accumulate tracks by going through all pages. There are multiple better tools to do so but I will give a raw example of it:
class TracksModel {
static func fetchAllPages(completion: #escaping ((_ tracks: [Track]?) -> Void)) {
var offset: Int = 0
let limit: Int = 50
var allTracks: [Track] = []
func appendPage() {
fetchSavedMusicPage(offset: offset, limit: limit) { tracks in
guard let tracks = tracks else {
completion(allTracks) // Most likely an error should be handled here
return
}
if tracks.count < limit {
// This was the last page because we got less than limit (50) tracks
completion(allTracks+tracks)
} else {
// Expecting another page to be loaded
offset += limit // Next page
allTracks += tracks
appendPage() // Recursively call for next page
}
}
}
appendPage() // Load first page
}
private static func fetchSavedMusicPage(offset: Int, limit: Int, completion: #escaping ((_ tracks: [Track]?) -> Void)) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { result in
switch result {
case .success(let model):
completion(model)
case .failure(let error):
print(error)
completion(nil) // Error also needs to call a completion
}
}
}
}
I hope comments will clear some things out. But the point being is that I nested an appendPage function which is called recursively until server stops sending data. In the end either an error occurs or the last page returns fewer tracks than provided limit.
Naturally it would be nicer to also forward an error but I did not include it for simplicity.
In any case you can now anywhere TracksModel.fetchAllPages { } and receive all tracks.
When you load and show your data (createSpinnerView) you also need to wait for data to be received before continuing. For instance:
func createSpinnerView() {
let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)
TracksModel.fetchAllPages { tracks in
DispatchQueue.main.async {
self.tracksArray = tracks
self.remove(asChildViewController: loadViewController)
self.navigateToFilterScreen(tracksArray: tracks)
}
}
}
A few components may have been removed but I hope you see the point. The method should be called on main thread already. But you are unsure what thread the API call returned on. So you need to use DispatchQueue.main.async within the completion closure, not outside of it. And also call to navigate within this closure because this is when things are actually complete.
Adding situation for fixed number of requests
For fixed number of requests you can do all your requests in parallel. You already did that in your code.
The biggest problem is that you can not guarantee that responses will come back in same order than your requests started. For instance if you perform two request A and B it can easily happen due to networking or any other reason that B will return before A. So you need to be a bit more sneaky. Look at the following code:
private func loadPage(pageIndex: Int, perPage: Int, completion: #escaping ((_ items: [Any]?, _ error: Error?) -> Void)) {
// TODO: logic here to return a page from server
completion(nil, nil)
}
func load(maximumNumberOfItems: Int, perPage: Int, completion: #escaping ((_ items: [Any], _ error: Error?) -> Void)) {
let pageStartIndicesToRetrieve: [Int] = {
var startIndex = 0
var toReturn: [Int] = []
while startIndex < maximumNumberOfItems {
toReturn.append(startIndex)
startIndex += perPage
}
return toReturn
}()
guard pageStartIndicesToRetrieve.isEmpty == false else {
// This happens if maximumNumberOfItems == 0
completion([], nil)
return
}
enum Response {
case success(items: [Any])
case failure(error: Error)
}
// Doing requests in parallel
// Note that responses may return in any order time-wise (we can not say that first page will come first, maybe the order will be [2, 1, 5, 3...])
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count) { // Start with all nil
didSet {
// Yes, Swift can do this :D How amazing!
guard responses.contains(where: { $0 == nil }) == false else {
// Still waiting for others to complete
return
}
let aggregatedResponse: (items: [Any], errors: [Error]) = responses.reduce((items: [], errors: [])) { partialResult, response in
switch response {
case .failure(let error): return (partialResult.items, partialResult.errors + [error])
case .success(let items): return (partialResult.items + [items], partialResult.errors)
case .none: return (partialResult.items, partialResult.errors)
}
}
let error: Error? = {
let errors = aggregatedResponse.errors
if errors.isEmpty {
return nil // No error
} else {
// There was an error.
return NSError(domain: "Something more meaningful", code: 500, userInfo: ["all_errors": errors]) // Or whatever you wish. Perhaps just "errors.first!"
}
}()
completion(aggregatedResponse.items, error)
}
}
pageStartIndicesToRetrieve.enumerated().forEach { requestIndex, startIndex in
loadPage(pageIndex: requestIndex, perPage: perPage) { items, error in
responses[requestIndex] = {
if let error = error {
return .failure(error: error)
} else {
return .success(items: items ?? [])
}
}()
}
}
}
The first method is not interesting. It just loads a single page. The second method now collects all the data.
First thing that happens is we calculate all possible requests. We need a start index and per-page. So the pageStartIndicesToRetrieve for case of 145 items using 50 per page will return [0, 50, 100]. (I later found out we only need count 3 in this case but that depends on the API, so let's stick with it). We expect 3 requests starting with item indices [0, 50, 100].
Next we create placeholders for our responses using
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count)
for our example of 145 items and using 50 per page this means it creates an array as [nil, nil, nil]. And when all of the values in this array turn to not-nil then all requests have returned and we can process all of the data. This is done by overriding the setter didSet for a local variable. I hope the content of it speaks for itself.
Now all that is left is to execute all requests at once and fill the array. Everything else should just resolve by itself.
The code is not the easiest and again; there are tools that can make things much easier. But for academical purposes I hope this approach explains what needs to be done to accomplish your task correctly.
Rx seem a bit fragile in that it closes down an entire chain if one single thing doesn't work. That has become a real problem in my code as I have a chain that requests parameters through ble. First we ask for ids, then definitions which is sort of mapping min and max values, lastly it asks for the actual parameters:
override func getParameters() -> Single<[ParameterModel?]> {
parameterCounter = 0
parameterDefinitionCounter = 0
return getParamterIds().do(onSuccess: { [weak self] values in
self?.numberOfParameterIds = Float(values?.count ?? 0)
})
.flatMap { ids in
Single.zip(ids!.compactMap { self.getParamterDefinition(id: $0) }) }
.flatMap { parameters in
Single.zip(parameters.compactMap { self.getParameter(id: $0!.id) }) }
}
So if we get an array with 30 parameter ids, it goes into getParamterDefinition(id: $0). And if it fails on a single one of those, which it does, the whole thing closes down and self.getParameter(id: $0!.id) is never run. So even though 29 parameters pass through getParamterDefinition(id: $0) nothing is passed to self.getParameter(id: $0!.id).
How do I recover from an error and keep going in the chain so that those that were successful in getParamterDefinition(id: $0) gets passed to self.getParameter(id: $0!.id) and gets displayed to the user?
*** UPDATE ***
This is the final result for anyone interested in solving issues like these:
override func getParameters() -> Single<[ParameterModel?]> {
parameterCounter = 0
parameterDefinitionCounter = 0
func getFailedParameter(id: Int) -> ParameterModel {
return ParameterModel(id: id, name: String(format: "tech_app_failed_getting_parameter".localized(), "\(id)"), min: 2000,
max: 21600000, defaultValue: 2500, value: 2500,
unit: "ms", access: 0, freezeFlag: 0,
multiplicator: 1, operatorByte: 1, brand: 0,
states: nil, didFailRetrievingParameter: true)
}
return getParamterIds().do(onSuccess: { [weak self] values in
self?.numberOfParameterIds = Float(values?.count ?? 0)
}).catchError { _ in return Single.just([]) }
.flatMap { ids in
Single.zip(ids!.compactMap { id in
self.getParamterDefinition(id: id).catchError { [weak self] _ in
self?.parameterErrorStatusRelay.accept(String(format: "tech_app_parameter_definition_error_status".localized(), "\(id)"))
return Single.just(getFailedParameter(id: id))
}
})
}
.flatMap { parameters in
Single.zip(parameters.compactMap { parameter in
guard let parameter = parameter, !(parameter.didFailRetrievingParameter) else {
return Single.just(parameter)
}
return self.getParameter(id: parameter.id).catchError { [weak self] _ in
self?.parameterErrorStatusRelay.accept(String(format: "tech_app_parameter_error_status".localized(), "\(parameter.id)"))
return Single.just(getFailedParameter(id: parameter.id))
}
})
}
}
You should use the Catch methods to handle errors, you can use these to stop your sequence from terminating when an error event occurs.
A simple example that just ignores any errors would be to return nil whenever your getParamterDefinition observable emits an error:
override func getParameters() -> Single<[ParameterModel?]> {
return getParameterIds()
.do(onSuccess: { [weak self] values in
self?.numberOfParameterIds = Float(values?.count ?? 0)
})
.flatMap { ids in
Single.zip(
ids!.compactMap {
self.getParameterDefinition(id: $0)?
.catchAndReturn(nil)
}
)
}
.flatMap { parameters in
Single.zip(
parameters.compactMap { parameter in
parameter.flatMap { self.getParameter(id: $0.id) }
}
)
}
}
I have a custom pipeline where I want to have 3 retry attempt for some error codes which are recoverable plus I want to add some short delay for the recoverable error. Anyone has an idea how I can do it?
func createRequest(for message: Message) -> AnyPublisher<ResponseMessage, Error> {
Future<ResponseMessage, Error> { promise in
.....
}
.tryCatch({ error -> AnyPublisher<ResponseMessage, Error> in
// If error is a recoverable error retry, otherwise fail directly
if case let MessageBusError.messageError(responseError) = error {
if responseError.isRecoverable {
// Make a next attempt only for recoverable error
throw error
}
}
//Should fail directly if the error code is not recoverable
return Fail<ResponseMessage, Error>(error: error)
.eraseToAnyPublisher()
})
.retry(3)
.eraseToAnyPublisher()
}
Basically, you need a retryIf operator, so you can provide a closure to tell Combine which errors should be retried, and which not. I'm not aware of such an operator, but it's not hard to build one for yourself.
The idiomatic way is to extend the Publishers namespace with a new type for your operator, and then extend Publisher to add support for that operator so that yo can chain it along with other operators.
The implementation could look like this:
extension Publishers {
struct RetryIf<P: Publisher>: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure
let publisher: P
let times: Int
let condition: (P.Failure) -> Bool
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
guard times > 0 else { return publisher.receive(subscriber: subscriber) }
publisher.catch { (error: P.Failure) -> AnyPublisher<Output, Failure> in
if condition(error) {
return RetryIf(publisher: publisher, times: times - 1, condition: condition).eraseToAnyPublisher()
} else {
return Fail(error: error).eraseToAnyPublisher()
}
}.receive(subscriber: subscriber)
}
}
}
extension Publisher {
func retry(times: Int, if condition: #escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}
}
Usage:
func createRequest(for message: Message) -> AnyPublisher<ResponseMessage, Error> {
Deferred {
Future<ResponseMessage, Error> { promise in
// future code
}
}
.retry(times: 3) { error in
if case let MessageBusError.messageError(responseError) = error, responseError.isRecoverable {
return true
}
return false
}
.eraseToAnyPublisher()
}
Note that I wrapped your Future within a Deferred one, otherwise the retry operator would be meaningless, as the closure will not be executed multiple times. More details about that behaviour here: Swift. Combine. Is there any way to call a publisher block more than once when retry?.
Alternatively, you can write the Publisher extension like this:
extension Publisher {
func retry(_ times: Int, if condition: #escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}
func retry(_ times: Int, unless condition: #escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
retry(times, if: { !condition($0) })
}
}
, which enables some funky stuff, like this:
extension Error {
var isRecoverable: Bool { ... }
var isUnrecoverable: Bool { ... }
}
// retry at most 3 times while receiving recoverable errors
// bail out the first time when encountering an error that is
// not recoverable
somePublisher
.retry(3, if: \.isRecoverable)
// retry at most 3 times, bail out the first time when
// an unrecoverable the error is encountered
somePublisher
.retry(3, unless: \.isUnrecoverable)
Or even funkier, ruby-style:
extension Int {
var times: Int { self }
}
somePublisher
.retry(3.times, unless: \.isUnrecoverable)
Typically, I try to avoid building new publishers, and instead prefer to compose publishers from built-in operators. I found it to be rather tricky to do here. Maybe someone can suggest a better approach.
Retry resubscribes on any failure, so to trick it, I packaged any non-recoverable errors into a Result value containing the error, but leaving recoverable errors as failures to .retry; then eventually unpack the Result back into the corresponding value/error.
Here's how it would work in your case:
func createRequest(for message: Message)-> AnyPublisher<ResponseMessage, Error> {
Future<ResponseMessage, Error> { promise in
.....
}
// pack a value into Result
.map { v -> Result<ResponseMessage, Error> in .success(v) }
.tryCatch { error -> AnyPublisher<Result<ResponseMessage, Error>, Error> in
if case let MessageBusError.messageError(responseError) = error {
if responseError.isRecoverable {
// Keep recoverable errors as failures
throw error
}
}
// pack a non-recoverable error into Result with a failure
return Just(.failure(error)).setFailureType(Error.self)
.eraseToAnyPublisher()
}
.retry(3)
// unpack back
.flatMap { result in result.publisher }
.eraseToAnyPublisher()
}
For completeness, to extend Publisher with the above approach:
extension Publisher {
private func retryOnly<U: Publisher>(
upstream: U,
retries: Int,
when predicate: #escaping (U.Failure) -> Bool
) -> AnyPublisher<U.Output, U.Failure> {
upstream
.map { v -> Result<U.Output, U.Failure> in .success(v) }
.catch { err -> AnyPublisher<Result<U.Output, U.Failure>, U.Failure> in
if predicate(err) {
return Fail(error: err).eraseToAnyPublisher()
} else {
return Just(.failure(err))
.setFailureType(to: U.Failure.self)
.eraseToAnyPublisher()
}
}
.retry(retries)
.flatMap { result in result.publisher }
.eraseToAnyPublisher()
}
func retry(_ retries: Int, when predicate: #escaping (Failure) -> Bool)
-> AnyPublisher<Output, Failure> {
return retryOnly(upstream: self, retries: retries, when: predicate)
}
}
failingPublisher.retry(3, when: { $0 is RecoverableError })
Coming from the RxJava background, I can not come up with a standard approach to implement sliding windows in RxSwift. E.g. I have the following sequence of events:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ...
Let's imagine event emission happens twice in a second. What I want to be able to do is to transform this sequence into a sequence of buffers, each buffer containing last three seconds of data. Plus, each buffer is to be emitted once in a second. So the result would look like that:
[1,2,3,4,5,6], [3,4,5,6,7,8], [5,6,7,8,9,10], ...
What I would do in RxJava is I would use one of the overloads of the buffer method like so:
stream.buffer(3000, 1000, TimeUnit.MILLISECONDS)
Which leads exactly to the result I need to accomplish: sequence of buffers, each buffer is emitted once in a second and contains last three seconds of data.
I checked RxSwift docs far and wide and I did not find any overloads of buffer operator which would allow me to do that. Am I missing some non-obvious (for RxJava user, ofc) operator?
I initially wrote the solution using a custom operator. I have since figured out how it can be done with the standard operators.
extension ObservableType {
func buffer(timeSpan: RxTimeInterval, timeShift: RxTimeInterval, scheduler: SchedulerType) -> Observable<[E]> {
let trigger = Observable<Int>.timer(timeSpan, period: timeShift, scheduler: scheduler)
.takeUntil(self.takeLast(1))
let buffer = self
.scan([Date: E]()) { previous, current in
var next = previous
let now = scheduler.now
next[now] = current
return next.filter { $0.key > now.addingTimeInterval(-timeSpan) }
}
return trigger.withLatestFrom(buffer)
.map { $0.sorted(by: { $0.key <= $1.key }).map { $0.value } }
}
}
I'm leaving my original solution below for posterity:
Writing your own operator is the solution here.
extension ObservableType {
func buffer(timeSpan: RxTimeInterval, timeShift: RxTimeInterval, scheduler: SchedulerType) -> Observable<[E]> {
return Observable.create { observer in
var buf: [Date: E] = [:]
let lock = NSRecursiveLock()
let elementDispoable = self.subscribe { event in
lock.lock(); defer { lock.unlock() }
switch event {
case let .next(element):
buf[Date()] = element
case .completed:
observer.onCompleted()
case let .error(error):
observer.onError(error)
}
}
let spanDisposable = scheduler.schedulePeriodic((), startAfter: timeSpan, period: timeShift, action: { state in
lock.lock(); defer { lock.unlock() }
let now = Date()
buf = buf.filter { $0.key > now.addingTimeInterval(-timeSpan) }
observer.onNext(buf.sorted(by: { $0.key <= $1.key }).map { $0.value })
})
return Disposables.create([spanDisposable, elementDispoable])
}
}
}
I have to make several api calls (approx 100) using a for loop and on completion of this I need to complete the Observable. I am using it as following:
func getMaterialInfo(materialNo:[String]) -> Observable<[String: Material]>{
return Observable.create({ (observable) -> Disposable in
for (index,mat) in materialNo.enumerated(){
// Pass the material number one by one to get the Material object
self.getMaterialInfo(materialNo: mat).subscribe(onNext: { material in
var materialDict: [String: Material] = [:]
materialDict[material.materialNumber] = material
observable.onNext(materialDict)
if index == (materialNo.count-1){
observable.onCompleted()
}
}, onError: { (error) in
observable.onError(error)
}, onCompleted: {
}).disposed(by: self.disposeBag)
}
return Disposables.create()
})
}
Although loop is working fine and observable.onCompleted() is called but the caller method does not receive it.
I am calling it like following:
private func getImage(materialNo:[String]){
if materialNo.isEmpty {
return
}
var dictMaterials = [String:String]()
materialService.getMaterialInfo(materialNo: materialNo).subscribe(onNext: { (materials) in
for (key,value) in materials{
if (value.imageUrl != nil){
dictMaterials[key] = value.imageUrl
}
}
}, onError: { (error) in
}, onCompleted: {
self.view?.updateToolImage(toolImageList: dictMaterials)
}, onDisposed: {}).disposed(by: disposeBag)
}
OnCompleted block of Rx is not executing. How can I fix it?
Edit (5-March)
I revisited this answer, because I'm not sure what my brain was doing when I wrote the code sample below. I'd do something like this instead:
func getMaterialInfo(materialNo: String) -> Observable<[String: Material]> {
// ...
}
func getMaterialInfo(materialNumbers:[String]) -> Observable<[String: Material]>{
let allObservables = materialNumbers
.map { getMaterialInfo(materialNo: $0) }
return Observable.merge(allObservables)
}
Original answer
From your code, I interpret that all individual getMaterialInfo calls are done concurrently. Based on that, I would rewrite your getMaterialInfo(:[_]) method to use the .merge operator.
func getMaterialInfo(materialNo:[String]) -> Observable<[String: Material]>{
return Observable.create({ (observable) -> Disposable in
// a collection of observables that we haven't yet subscribed to
let allObservables = materialNo
.map { getMaterialInfo(materialNo: $0) }
return Observable.merge(allObservables)
}
return Disposables.create()
}
Note that using merge subscribes to all observable simultaneously, triggering 100 network requests at the same time. For sequential subscription, use concat instead!