RxSwift how to conditionally control when a button emits taps?
Asked Answered
K

1

6

I want to prevent my UIButtons from emitting RX tap events until some control stream indicates that it is ready. I want buttons to be disabled again if the control stream reverts back to not ready. Control sequence is a ReplaySubject and always has value

I've tried skipUntil, but that's a one shot operation - once the control sequence emits, it cannot go back to disabled state - all button presses will go through, ignoring the control sequence

How can I restrict a UIButton from sending tap events unless some other stream has a certain value?

let enableButtons = configStatusInputSequence
.filter { (configured, ready) -> Bool in
    return configured && ready
}

for button in controlButtons{
    button.rx.tap
        .skipUntil(enableButtons)
        .bind(to: commandOutputSequence)
        .disposed(by: bag)
}
Kryska answered 9/9, 2018 at 11:4 Comment(4)
How about binding the isEnabled property? This way the buttons won't be able to emit at allSpeciality
@Speciality are you suggesting binding the “enable buttons” control stream to buttons “Rx.isEnabled”?Kryska
Ideally I keep the buttons enabled, and direct their output down another path, such as displaying an error message or bringing up the configuration screenKryska
You could do that, and bind the action instead based on the state of the other observables.Speciality
D
7

Here's something I wrote a while back and reproduced below. If you look at my gist (https://gist.github.com/danielt1263/1a70c4f7b8960d06bd7f1bfa81802cc3) you will see that I originally wrote it as a custom operator. Later I learned that the combination of built in operators below do the same job.

If nothing else, looking back at the older revisions in the gist will give you the sense of how to write your own operators.

extension ObservableType {

    /**
     Filters the source observable sequence using a trigger observable sequence producing Bool values.
     Elements only go through the filter when the trigger has not completed and its last element was true. If either source or trigger error's, then the source errors.
     - parameter trigger: Triggering event sequence.
     - returns: Filtered observable sequence.
     */
    func filter(if trigger: Observable<Bool>) -> Observable<E> {
        return withLatestFrom(trigger) { (myValue, triggerValue) -> (Element, Bool) in
                return (myValue, triggerValue)
            }
            .filter { (myValue, triggerValue) -> Bool in
                return triggerValue == true
            }
            .map { (myValue, triggerValue) -> Element in
                return myValue
            }
    }
}

If you want to change what the button does depending on the value of your control observable, setup two filters. Normally the filter only passes through taps when the enableButtons emits true. Use a map to reverse it in the second case and direct button taps down another path:

button.rx.tap.filter(if: enableButtons)
    .subscribe(onNext: { /* do one thing when enableButtons emits true */ }
    .disposed(by: bag)

button.rx.tap.filter(if: enableButtons.map { !$0 })
    .subscribe(onNext: { /* do other thing when enable buttons emits false*/ }
    .disposed(by: bag)
Dangerfield answered 9/9, 2018 at 18:1 Comment(7)
I've looked up withLatestFrom and still have trouble following why this would work :(Kryska
withLatestFrom works like combineLatest except that the latter emits whenever either observable emits, while the former only emits an element when the primary observable emits. If you look up the previous revision in the gist, you can see the inner workings.Dangerfield
Here you go. I used a name tuple in the answer to make what's going on more obvious.Dangerfield
Great! I have edited the example to clarify - I could not read the filter function with all that $ variables in place.Kryska
@AlexStone If you are going to edit my code, at least make something that compiles. Thanks.Dangerfield
I fixed it - sorry, those dollar things are write only code. I can see how you got there now, but original impression is overwhelming.Kryska
Thank you Daniel, yet another "aha!" moment you've given me in the world of RxSwift! withLatestFrom isn't mentioned much in the rx function cheatsheets.. Sadly your aforementioned gist is down - any plans to put it back up? A "sense of how to write my own operators" sounds very useful!Phuongphycology

© 2022 - 2024 — McMap. All rights reserved.