After filtering an Observable list, I might have an empty list. I'm only interested in events that contain a populated list. Is there any way to stop empty events propagating to onNext?
let source: BehaviorRelay<[Int]> = .init(value: [])
source
.map { nums -> [Int] in
return nums.filter { $0 < 10 }
}
/// What can go here to stop/block/ignore an empty list
.subscribe(onNext: { nums in
print("OKPDX \(nums)")
})
source.accept([1, 9, 13])
// prints "[1, 9]" (all good)
source.accept([20, 22, 101])
// prints "[]" (not desirable, I'd rather not know)
What about using .filter? You could do this:
.map { nums -> [Int] in
return nums.filter { $0 < 10 }
}
.filter { !$0.isEmpty }
Or you could also validate that whenever you get an event like this:
.subscribe(onNext: { nums in
guard !nums.isEmpty else { return }
print("OKPDX \(nums)")
})
Related
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'm observing a BehaviorRelay and I want to subscribe only when the number of items increase. I tried distinct/dinstinctUntilChanged but it does not suit my needs because it will skip too much or too few times.
behaviorRelay
.compactMap { $0?.items }
.subscribe(onNext: { elements in
print("items has one more element.")
}).disposed(by: bag)
var behaviorRelay = BehaviorRelay<[Car]?>(value: [])
class Car {
var items: [Any] // whatever... just an example.
}
First of all, use map to map from array to number (of elements):
.map { $0?.count ?? 0 } // return 0 if array is nil
Than use scan, to retrieve both current and previous element, like this:
.scan((0, 0)) { previousPairOfValues, newValue in
return (previousPairOfValues.1, newValue) // create new pair from newValue and second item of previous pair
}
Then use filter, to only pass increasing values:
.filter { $0.1 > $0.0 } // newer value greater than older value
Than map it back to the latest value:
.map { $0.1 }
Putting all together:
behaviorRelay
.compactMap { $0?.items }
.map { $0?.count ?? 0 } // return 0 if array is nil
.scan((0, 0)) { previousPairOfValues, newValue in
return (previousPairOfValues.1, newValue) // create new pair from newValue and second item of previous pair
}
.filter { $0.1 > $0.0 } // newer value greater than older value
.map { $0.1 }
.subscribe(onNext: { elementCount in
print("items has one more element.")
print("there are \(elementCount) items now")
}).disposed(by: bag)
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 a newBid object, that contains some data and images array. I want to upload all images and data to the server and zip those upload observables. If I create separate drivers for data, image1 and image2, I succeed.
But what I want to really do is to not hardcode images since array may contain from 0 to 10 images. I want to create observables array programmatically and zip them.
let dataSaved = saveTaps.withLatestFrom(newBid)
.flatMapLatest { bid in
return CustomerManager.shared.bidCreate(bid: bid)
.trackActivity(activityIndicator)
.asDriver(onErrorJustReturn: false)
}
let photoSaved0 = saveTaps.withLatestFrom(newBid)
.flatMapLatest { bid in
return CustomerManager.shared.bidUploadFile(image: bid.images[0])
.trackActivity(activityIndicator)
.asDriver(onErrorJustReturn: false)
}
let photoSaved1 = saveTaps.withLatestFrom(newBid)
.flatMapLatest { bid in
return CustomerManager.shared.bidUploadFile(image: bid.images[1])
.trackActivity(activityIndicator)
.asDriver(onErrorJustReturn: false)
}
saveCompleted = Driver.zip(dataSaved, photoSaved0, photoSaved1){ return $0 && $1 && $2 }
/*
// 0. Getting array of images from newBid
let images = newBid.map { bid in
return bid.images
}
// 1. Creating array of upload drivers from array of images
let imageUploads = images.map { (images: [UIImage]) -> [Driver<Bool>] in
var temp = [Driver<Bool>]()
return temp
}
// 2. Zipping array of upload drivers to photoSaved driver
photoSaved = Driver
.zip(imageUploads) // wait for all image requests to finish
.subscribe(onNext: { results in
// here you have every single image in the 'images' array
results.forEach { print($0) }
})
.disposed(by: disposeBag)*/
In the commented section if I try to zip imageUploads, I get error:
Argument type 'SharedSequence<DriverSharingStrategy, [SharedSequence<DriverSharingStrategy, Bool>]>' does not conform to expected type 'Collection'
How about something like this?
let saves = saveTaps.withLatestFrom(newBid)
.flatMapLatest { (bid: Bid) -> Observable<[Bool]> in
let dataSaved = CustomerManager.shared.bidCreate(bid: bid)
.catchErrorJustReturn(false)
let photosSaved = bid.images.map {
CustomerManager.shared.bidUploadFile(image: $0, bidID: bid.id)
.catchErrorJustReturn(false)
}
return Observable.zip([dataSaved] + photosSaved)
.trackActivity(activityIndicator)
}
.asDriver(onErrorJustReturn: []) // remove this line if you want an Observable<[Bool]>.
Final solution
let bidID: Driver<Int> = saveTaps.withLatestFrom(newBid)
.flatMapLatest { bid in
return CustomerManager.shared.bidCreate(bid: bid)
.trackActivity(activityIndicator)
.asDriver(onErrorJustReturn: 0)
}
saveCompleted = Driver.combineLatest(bidID, newBid) { bidID, newBid in
newBid.uploadedImages.map {
CustomerManager.shared.bidUploadFile(image: $0, bidID: bidID).asDriver(onErrorJustReturn: false)
}
}.flatMap { imageUploads in
return Driver.zip(imageUploads).trackActivity(activityIndicator).asDriver(onErrorJustReturn: [])
}.map{ (results:[Bool]) -> Bool in
return !results.contains(false)
}
It is a combined version of this which is equivalent:
let imageUploads: Driver<[Driver<Bool>]> = Driver.combineLatest(bidID, newBid) { bidID, newBid in
newBid.uploadedImages.map {
CustomerManager.shared.bidUploadFile(image: $0, bidID: bidID).asDriver(onErrorJustReturn: false)
}
}
let photosSaved: Driver<[Bool]> = imageUploads.flatMap { imageUploads in
return Driver.zip(imageUploads).trackActivity(activityIndicator).asDriver(onErrorJustReturn: [])
}
saveCompleted = photosSaved.map{ (results:[Bool]) -> Bool in
return !results.contains(false)
}
Using RxSwift, say I have a class A that contains an observable of integer
class A: {
let count: Observable<Int>
}
and a observable collection of objects of A
let data: Observable<[A]>
I want to define a sum: Observable<Int> that will be the sum of all the count on all the objects in data. Whenever the data observable collection changes or any of the count property changes the sum should also change.
How to achieve this? I tried some flapMap and map combinations but only got to a solution when sum gets updated only when data gets updated, ignoring the count changes.
let sum = data.flatMap { Observable.from($0).flatMap { $0.count }.reduce(0, accumulator: +) }
'.reduce()' emits only on completion, so it has to be inside of the outer '.flatMap()'
Update 1:
let sumSubject: BehaviorSubject<Observable<Int>> = BehaviorSubject.create(Observable.empty())
let sum = sumSubject.switchLatest()
// every time it has to be a new 'data' observable!
sumSubject.onNext(data.flatMap { Observable.from($0).flatMap { $0.count }.reduce(0, accumulator: +) })
Update 2:
let counts: Observable<[Int]> = data.flatMap { Observable.combineLatest($0.map { $0.count }) }
let sum: Observable<Int> = counts.map { $0.reduce(0) { $0 + $1 } }