5

I have three buttons and I want them to be selected only one at a time:

enter image description here

and:

enter image description here

etc...

My approach is this:

class MyController: UIViewController {

    @IBOutlet var buttonOne: UIButton!
    @IBOutlet var buttonTwo: UIButton!
    @IBOutlet var buttonThree: UIButton!

    var buttonOneIsSelected = Variable(true)
    var buttonTwoIsSelected = Variable(false)
    var buttonThreeIsSelected = Variable(false)


    override func viewDidLoad() {
        super.viewDidLoad()

        buttonOne.isSelected = true

        buttonOneIsSelected.asDriver()
            .drive(buttonOne.rx.isSelected)
            .disposed(by: disposeBag)
        buttonTwoIsSelected.asDriver()
            .drive(buttonTwo.rx.isSelected)
            .disposed(by: disposeBag)
        buttonThreeIsSelected.asDriver()
            .drive(buttonThree.rx.isSelected)
            .disposed(by: disposeBag)

        buttonOne.rx.tap.asObservable().map { (_) -> Bool in
            return !self.buttonOne.isSelected
        }
        .do(onNext: { (isSelected) in
            self.buttonTwoIsSelected.value = !isSelected
            self.buttonThreeIsSelected.value = !isSelected
        })
        .bindTo(buttonOne.rx.isSelected)
        .disposed(by: disposeBag)

        buttonTwo.rx.tap.asObservable().map { (_) -> Bool in
            return !self.buttonTwo.isSelected
            }
            .do(onNext: { (isSelected) in
                self.buttonOneIsSelected.value = !isSelected
                self.buttonThreeIsSelected.value = !isSelected
            })
            .bindTo(buttonTwo.rx.isSelected)
            .disposed(by: disposeBag)

        buttonThree.rx.tap.asObservable().map { (_) -> Bool in
            return !self.buttonThree.isSelected
            }
            .do(onNext: { (isSelected) in
                self.buttonOneIsSelected.value = !isSelected
                self.buttonTwoIsSelected.value = !isSelected
            })
            .bindTo(buttonThree.rx.isSelected)
            .disposed(by: disposeBag)
}

Is there a better approach? It works, but is there a better 'reactive' way to do it by using RxSwift?

TheoK
  • 3,601
  • 5
  • 27
  • 37

1 Answers1

16

Subject and by extension Variable are most of the time only useful when bridging from imperative to reactive world. Here, you could do without them.

.do(onNext:) is also a way to perform side effect, something you usually don't want in your reactive code.

// force unwrap to avoid having to deal with optionals later on
let buttons = [button1, button2, button3].map { $0! }

// create an observable that will emit the last tapped button (which is
// the one we want selected)
let selectedButton = Observable.from(
  buttons.map { button in button.rx.tap.map { button } }
).merge()

// for each button, create a subscription that will set its `isSelected`
// state on or off if it is the one emmited by selectedButton
buttons.reduce(Disposables.create()) { disposable, button in
    let subscription = selectedButton.map { $0 == button }
      .bindTo(button.rx.isSelected)

    // combine two disposable together so that we can simply call
    // .dispose() and the result of reduce if we want to stop all
    // subscriptions
    return Disposables.create(disposable, subscription)
}
.addDisposableTo(disposeBag)
tomahh
  • 13,441
  • 3
  • 49
  • 70
  • Thank for the answer. I am pretty new to reactive programming. The reduce operator is a bit difficult for me to grasp. Do you have any explanatory resources for RxSwift in general (or specifically for the reduce operator) in order to expand my knowledge? – TheoK Feb 19 '17 at 10:47
  • `reduce` is actually a regular swift sequence method. It takes a seed value and an accumulator. The simplest example is `[1, 2, 3].reduce(0) { $0 + $1 }`. This will compute `((0 + 1) + 2) + 3 = 6`. In our case, the result can be think of as `Disposable.create(Disposable.create(Disposable.create(emptyDisposable, button1Disposable), button2Disposable), button3Disposable)` – tomahh Feb 19 '17 at 11:36
  • 1
    Great @tomahh . If I understood correctly the reduce method gives you a chance to apply a function to every element of the array, and then create a single subscription instead of creating one on every element, right? And the reason you are returning a Disposable is because the bindTo method creates a Disposable? The implementation inside reduce baffled me a bit. (Sorry for the questions but I want to understand the rationale behind the implementation:) ) – TheoK Feb 20 '17 at 08:21
  • You can see reduce as a loop. I've put up a [gist](https://gist.github.com/60740a28d447e756f4ac5978f4202888) where the code is written in a for loop, maybe it will be easier for you to grasp :) – tomahh Feb 23 '17 at 00:29
  • Really nice solution! – denis631 Mar 12 '17 at 20:08
  • 1
    I am new to rxSwift. Can you explain why you use buttons.reduce with "Disposables.create"? I also want to add facility like if button is already selected then deselect it.How can I achive this? – parth Sep 14 '17 at 13:06
  • When I try this solution I can see in logs that is working, but it has no effect on Buttons. Any idea? – Vanya Apr 18 '20 at 09:37