Search code examples
swiftrx-swiftrx-cocoa

Disabling a button based on value in a UITextField only works once (RxSwift)


I'm trying to get to grips with RxCocoa and have experienced an unusual bug relating to some dynamic UI behaviour I'm trying to implement.

I have a UITextField that's used for user input. The button which adds the input to a Realm database is bound to an RxSwift Action. This works absolutely fine.

Initially, I disabled the button until there was text of at least 1 character in length in the UITextField - the code of this works fine. The bug in my code arose when I then added a subscription to the Action's executionObservables parameter that should clear the UITextField after the button is pressed.

Expected behaviour:

  • No text (initial state) > button disabled
  • Text entered > button enabled
  • Text entered and button pressed > text field cleared and button disabled

Actual behaviour:

  • No text (initial state) > button disabled
  • Text entered > button enabled
  • Text entered and button pressed > text field cleared BUT button remains enabled

Adding debug() indicates that the binding to the UITextField that disables the button is disposed but I can't figure out why as the UIViewController and its associated view model should still be in scope. Can anyone point me in the right direction?

Code snippet:

func bindViewModel() {
    // populate table
    viewModel.output.sectionedObservations
        .drive(tableView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)

    // only allow enable button when there is text in the textfield
    observationTextField.rx.text
        .debug()
        .map { $0!.count > 0 }
        .bind(to: addObservationButton.rx.isEnabled)
        .disposed(by: disposeBag)

// clear textfield once Action triggered by button press has completed
viewModel.addObservation.executionObservables
    .subscribe({ [unowned self] _ in
        self.observationTextField.rx.text.onNext("")
})
.disposed(by: disposeBag)

// add Observation to Realm using Action provided by the view model
addObservationButton.rx.tap
    .withLatestFrom(observationTextField.rx.text.orEmpty)
    .take(1)
    .bind(to: viewModel.addObservation.inputs)
    .disposed(by: disposeBag)
}

Solution

  • I think there is a little misunderstanding about how ControlProperty trait behaves. Let's take a look at specific behavior which is Programmatic value changes won't be reported

    This Observable observationTextField.rx.text after subscription will not emit event for both:

    self.observationTextField.rx.text.onNext("")

    or

    self.observationTextField.text = ""

    I have 2 suggestion for your code:

    1) Do the job manually:

    viewModel.addObservation.executionObservables
        .subscribe({ [unowned self] _ in
            self.observationTextField = ""
            self.addObservationButton.isEnabled = false
    })
    .disposed(by: disposeBag)
    

    2) Add one more Observable and subscription:

    //a
        viewModel.addObservation.executionObservables
          .map { _ in return "" }
          .bind(to: observationTextField.rx.text)
          .disposed(by: disposeBag)
    
        viewModel.addObservation.executionObservables
          .map { _ in return false }
          .bind(to: addObservationButton.rx.isEnabled)
          .disposed(by: disposeBag)
    
    //b
        let executionObservables = viewModel.addObservation
          .executionObservables
          .share()
    
        executionObservables
          .map { _ in return "" }
          .bind(to: observationTextField.rx.text)
          .disposed(by: disposeBag)
    
        executionObservables
          .map { _ in return false }
          .bind(to: addObservationButton.rx.isEnabled)
          .disposed(by: disposeBag)
    

    Not sure how Action is implemented, to prevent job done twice maybe you have to share resources.