Subscription call 2 times, how to unsubscribe observable in RxSwift - swift

Reset password call occurs 2 times, how can I remove the subscription in that block.
let digits = [firstDigit, secondDigit, thirdDigit, fourthDigit, fifthDigit, sixthDigit]
let digitsValid = digits.map { $0?.textField.rx.text.orEmpty.map({ $0.count == 1 }).share(replay: 1) }
let allDigitsFilled = Observable.combineLatest(digitsValid.map{ $0 ?? BehaviorRelay(value: false).asObservable()}).share(replay: 1)
allDigitsFilled.subscribe (onNext: { [weak self] (boolArray) in
guard let self = self else { return }
let isFilled = boolArray.allSatisfy({ $0 })
if isFilled {
self.viewModel.resetPassword()
}
}).disposed(by: disposeBag)

Your onNext block will be called every time any of the text fields changes its contents. Your first goal should be to make it so the block is only called when you want to reset the password.
So first, you want to put the .allSatisfy inside an Observable.filter call so your onNext block will only be called when all six text fields contain exactly 1 character. Once you do that, then you can simply use .take(1) which will complete the subscription once a next value is emitted.
Something like this should do it, and looks quite a bit cleaner:
let digits = [firstDigit, secondDigit, thirdDigit, fourthDigit, fifthDigit, sixthDigit]
let texts = digits.compactMap { $0?.textField.rx.text.orEmpty }
Observable.combineLatest(texts)
.filter { $0.allSatisfy { $0.count == 1 } }
.map { _ in }
.take(1)
.subscribe(onNext: { [viewModel] in
viewModel?.resetPassword()
})
.disposed(by: disposeBag)

Related

How to capture the first value in a publisher one way and the rest another way

I have a publisher where I want to handle receiving the first value one way, and receiving the rest another way. Currently I have the following:
let bar = foo
.first()
.sink { value in
print("first value is \(value)")
}
let baz = foo
.dropFirst()
.sink { value in
print("one of the rest is \(value)")
}
foo.send(1)
foo.send(2)
foo.send(3)
foo.send(4)
foo.send(5)
foo.send(6)
This works. But it forces me to have two subscriptions, one for the first value, and another for the rest. Is there a clever way of combining the two into one subscription (without external flag management). Something like:
let bar = foo
.first()
.sink { value in
print("first value is \(value)")
}
.dropFirst()
.sink {
print("the rest is \(value)")
}
I'm aware the above doesn't make sense (and is probably uglier than the original solution) since a publisher is singular flow that doesn't branch (beyond value, completion, error), but that's due to my lack of creativity, I'm hoping someone here has something that I might be overlooking.
One typical approach is to use something like .scan to map into a tuple that tells the rest of the pipeline whether this is the first one or not.
In this simple example, the "rest of the pipeline" is simply .sink, but you can see that it does know which one is first.
class ViewController: UIViewController {
var cancellables = Set<AnyCancellable>()
let subject = PassthroughSubject<Int, Never>()
override func viewDidLoad() {
super.viewDidLoad()
var firstPassed = false
subject
.scan((0, false)) {prev, this in
defer { firstPassed = true }
return (this, firstPassed)
}
.sink {
if !$0.1 {
print($0.0, "is the first one")
} else {
print($0.0, "is not the first one")
}
}
.store(in: &cancellables)
subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4)
subject.send(5)
}
}
Result:
1 is the first one
2 is not the first one
3 is not the first one
4 is not the first one
5 is not the first one
var cancellables = Set<AnyCancellable>()
let foo = PassthroughSubject<Int, Never>()
let first = foo
.first()
let restOfFirst = foo
.dropFirst()
Publishers.CombineLatest(first, restOfFirst) // <= here
.sink {
print("first value: \($0), rest Of other values\($1)")
}
.store(in: &cancellables)
Resualt:
foo.send(1)
foo.send(2)
foo.send(3)
foo.send(4)
foo.send(5)
foo.send(6)
first value: 1, rest Of other values: 2
first value: 1, rest Of other values: 3
first value: 1, rest Of other values: 4
first value: 1, rest Of other values: 5
first value: 1, rest Of other values: 6

Why does skipWhile behave differently in these examples?

In the first example, my terminal outputs
next(1)
next(2)
next(3)
next(4)
next(5)
next(6)
next(7)
next(8)
next(9)
next(10)
completed
In the second example it will output
next(3)
next(6)
next(8)
next(9)
completed
I know the values are different in each, but I would expect the first example to have had filtering applied so it follows the logic within my .skipWhile { $0 % 2 == 0 } block
func skipWhile() {
let bag = DisposeBag()
Observable
.from(Array(1...10))
.skipWhile { $0 % 2 == 0 }
.subscribe { print($0) }
.disposed(by: bag)
Observable
.from([2,3,6,8,9])
.skipWhile { $0 % 2 == 0 }
.subscribe { print($0) }
.disposed(by: bag)
}
skipWhile()
skipWhile is not filter. It skips elements at the start of the observable's lifetime while the predicate as true. As soon as an element comes along that no longer satisfies the predicate, it opens the flood gates and let everything else through.
Your first observable says "skip everything until the first odd number". The first element is odd, so nothing is skipped, and that's why you see all array elements being printed.
If you notice in your second observable, you didn't filter out even numbers (because there's an 8). You merely skipped over elements until the first odd number (3), causing the 2 to be skipped.
On a side note
Int.isMultiple(of: ) has been added in Swift 5, and I suggest you use it in cases like this. It just makes it clearer, and side-steps errors caused by misreading == vs !=.
Observable
.from(Array(1...10))
.skipWhile { $0.isMultiple(of: 2) }
.subscribe { print($0) }
.disposed(by: bag)
You could even name your predicate:
let isEven: (Int) -> Bool = { $0.isMultiple(of: 2) }
Observable
.from(Array(1...10))
.skipWhile(isEven)
.subscribe { print($0) }
.disposed(by: bag)
Or my favourite, add it as a computed property:
extension BinaryInteger {
var isEven: Bool { return self.isMultiple(of: 2) }
var isOdd: Bool { return !self.isEven }
}
Observable
.from(Array(1...10))
.skipWhile(\.isEven)
.subscribe { print($0) }
.disposed(by: bag)

Skip an event from source observable if a new event from a given observable was received in a given time interval

I'm trying to write a method on UIView extension, which will observe long press on a given view. I know it can be done using UILongPressGestureRecognizer, but I really want to figure out the question and do it this way.
I tried to use takeUntil operator, but it completes an observable, but I need to skip the value and receive further events.
The question can be also transformed to: How to omit completed event and keep receiving further events?
func observeLongPress(with minimumPressDuration: Double = 1) ->
Observable<Void> {
let touchesBeganEvent = rx.methodInvoked(#selector(touchesBegan))
let touchesEndedEvents = [#selector(touchesEnded), #selector(touchesCancelled)]
.map(rx.methodInvoked)
let touchesEndedEvent = Observable.merge(touchesEndedEvents)
return touchesBeganEvent
.observeOn(MainScheduler.instance)
.delay(minimumPressDuration, scheduler: MainScheduler.instance)
.takeUntil(touchesEndedEvent)
.map { _ in }
}
This will work, but will complete the whole sequence (as it intended to do).
The answer if floating around (as it always do), but after a few hours I decided to ask. :)
Update
The floating answer just flew inside (~15 mins for doing so), but I'm still interested in answer, because maybe there's something that I'm missing here.
func observeLongPress(with minimumPressDuration: Double = 1) -> Observable<Void> {
let touchesBeganEvent = rx.methodInvoked(#selector(touchesBegan))
let touchesEndedEvents = [#selector(touchesEnded), #selector(touchesCancelled)]
.map(rx.methodInvoked)
let touchesEndedEvent = Observable.merge(touchesEndedEvents)
return touchesBeganEvent
.observeOn(MainScheduler.instance)
.flatMapLatest { _ -> Observable<Void> in
return Observable.just(())
.delay(minimumPressDuration, scheduler: MainScheduler.instance)
.takeUntil(touchesEndedEvent)
.void()
}
}
Your Updated code won't work. Even if you don't emit the completed event out of the function, it still got emitted from the takeUntil and therefore that operator won't emit any more values.
That said, this idea can be accomplished. Since you said you want to learn, I'll talk through my entire thought process while writing this.
First let's outline our inputs and outputs. For inputs we have two Observables, and a duration, and whenever we are dealing with duration we need a scheduler. For outputs we only have a single Observable.
So the function declaration looks like this:
func filterDuration(first: Observable<Void>, second: Observable<Void>, duration: TimeInterval, scheduler: SchedulerType) -> Observable<Void> {
// code goes here.
}
We are going to be comparing the time that the two observables fire so we have to track that:
let firstTime = first.map { scheduler.now }
let secondTime = second.map { scheduler.now }
And since we are comparing them, we have to combine them somehow. We could use merge, combineLatest, or zip...
combineLatest will fire whenever either Observable fires and will give us the latest values from both observables.
zip will mate, 1 for 1, events from both observables. This sounds intriguing, but would break down if one of the observables fires more often than the other so it seems a bit brittle.
merge will fire when either of them fire, so we would need to track which one fired somehow (probably with an enum.)
Let's use combineLatest:
Observable.combineLatest(firstTime, secondTime)
That will give us an Observable<(RxTime, RxTime)> so now we can map over our two times and compare them. The goal here is to return a Bool that is true if the second time is greater than the first time by more than duration.
.map { arg -> Bool in
let (first, second) = arg
let tickDuration = second.timeIntervalSince(first)
return duration <= tickDuration
}
Now the above will fire every time either of the two inputs fire, but we only care about the events that emit true. That calls for filter.
.filter { $0 } // since the wrapped value is a Bool, this will accomplish our goal.
Now our chain is emitting Bools which will always be true but we want a Void. How about we just throw away the value.
.map { _ in }
Putting it all together, we get:
func filterDuration(first: Observable<Void>, second: Observable<Void>, duration: TimeInterval, scheduler: SchedulerType) -> Observable<Void> {
let firstTime = first.map { scheduler.now }
let secondTime = second.map { scheduler.now }
return Observable.combineLatest(firstTime, secondTime)
.map { arg -> Bool in
let (first, second) = arg
let tickDuration = second.timeIntervalSince(first)
return duration <= tickDuration
}
.filter { $0 }
.map { _ in }
}
The above isolates our logic and is, not incidentally, easy to test with RxTests. Now we can wrap it up into a UIView (that would be hard to test.)
func observeLongPress(with minimumPressDuration: Double = 1) -> Observable<Void> {
let touchesBeganEvent = rx.methodInvoked(#selector(touchesBegan)).map { _ in }
let touchesEndedEvents = [#selector(touchesEnded), #selector(touchesCancelled)]
.map(rx.methodInvoked)
let touchesEndedEvent = Observable.merge(touchesEndedEvents).map { _ in }
return filterDuration(first: touchesBeganEvent, second: touchesEndedEvent, duration: minimumPressDuration, scheduler: MainScheduler.instance)
}
There you go. One custom operator.

How to merge String & Int to get in ONE flatMap

enum Input { case text(String); case page(Int) }
I am managing pagination with keyword search to API method.
Now I either can pass keywords or page number, but not both at same time in Rx.
I have written following code with help of some existing available gist
let start = Observable.merge(reload, loadNext)
let stringObservable = keyword.asObservable().map { Input.text($0) }
let intObservable = start.asObservable().map { Input.page($0) }
let request_call = Observable.of(stringObservable, intObservable).merge()
let page = request_call
.flatMap { input in
Observable.combineLatest(Observable.just($0), api.loadData(page: $0, keyword: "breaking")) { (pageNumber: $0, items: $1) }
.materialize()
.filter { $0.isCompleted == false }
}
.share()
start keep Page Number, & keyword keeps search keywords.
I need to merge both, I did using ENUM & Merge,
Now I have to call API, but showing as Input,
So How can I get both values in one flatMap
Get rid of the Input enum and use combineLatest instead of merge.
Then request_call will be an Observable<(String, Int)> and you can use the two values in the loadData function.
let start = Observable.merge(reload, loadNext)
let stringObservable = keyword.asObservable()
let intObservable = start.asObservable()
let request_call = Observable.combineLatest(stringObservable, intObservable)
let page = request_call
.flatMap { text, page in
Observable.combineLatest(Observable.just(page), api.loadData(page: page, keyword: text)) { (pageNumber: $0, items: $1) }
.materialize()
.filter { $0.isCompleted == false }
}
.share()

How do I emulate a computed property with access to the latest value in RxSwift?

If I want to emulate a standard property of e.g a Bool in RxSwift I can use let isValid = Variable<Bool>(false) and then use .value to get the last value inline and .asObservable() to access the stream.
However I want to emulate a computed propery e.g. var isValid { return self.password.characters.count > 0 } and also be able to get the last value inline as well as in the form of an observable stream.
I want to be able to do both so I can write code like ...
if isValid.value { // isValid is Variable<Bool>
// ... do something ....
}
as well as bind to e.g. a TextField
I know I can write as a pure Observable as follows ...
var isValid: Observable<Bool> {
return self.username.asObservable().map { username in // username is Variable<String>
return username.characters.count > 0
}
}
but then I have to refactor the previous example to be ....
isValid.subscribe { isValid in
if isValid.element {
// ... do something ....
}
}.dispose(of: self.disposeBag)
How then do I express a computed property in RxSwift which can be consumed as an inline value as well as a stream?
I had the same problem and ended up with a solution that does not look clean but works. I would add a new var valid: Bool = false and a subscription to the isValid observable that updates it
isValid.subscribe(onNext:{ [weak self] value in self?.valid = valid})
With this isValid is used for binding and subscriptions and valid for use in imperative code.
Btw your definition of isValid is kind of "unnatural", just make it a let isValid: Observable<Bool> and assign it in init like isValid = username.asObservable().map({$0.characters.count > 0})
Update:
Another solution is using RxBlockling, you can get the current value with try! isValid.toBlocking().single() but I think it is a worse solution and generally not recommended.
Try this:
let username = Variable("")
private let _isValid: Observable<Bool> = username.asObservable().map{ $0.characters.count > 0 }
let isValid = Variable(false)
Then, in your init, viewDidLoador wherever:
_isValid.asObservable()
.bind(to: isValid)
.disposed(by: bag)
You can test it with:
isValid.asObservable()
.subscribe(onNext: {
print("value: \($0)")
})
.disposed(by: disposeBag)
username.value = "xfreire"
// OUTPUTS:
// value: false
// value: true
and you are still able to do:
if isValid.value {
// ...
}