0

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)
}
Ben Rockey
  • 920
  • 6
  • 23
rustproofFish
  • 931
  • 10
  • 32
  • 1
    Aside from you question - this `$0!.count > 0` is a crash waiting to happen. Replace it with a safe operation like . `($0?.count ?? 0) > 0` – AgRizzo May 31 '19 at 13:29
  • I'm aware but thanks for highlighting. This was quick, experimental code and definitely needs refactoring to take care of the dreaded optional! – rustproofFish May 31 '19 at 13:54
  • If I understand you correctly, you ultimately need the viewModel to tell the `observationTextField` what its value should be. I got into the habit of creating a `Driver` for every output of the viewModel, then bind that to your textfield. Use that to replace your `viewModel.addObservation.executionObservables` – AgRizzo May 31 '19 at 14:08
  • I do need to programatically amend the contents of the UITextField but only to erase it so I wouldn't have thought that I need a direct input from the view model. Using executionObservables ensures that the Action has been performed correctly and, as the amendment is to set the .text property to "", I thought a simple approach was best – rustproofFish May 31 '19 at 14:30

1 Answers1

1

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.

  • Thanks for the steer Sergey. That did the trick and now I understand what I did wrong (and hopefully make that mistake again!) – rustproofFish May 31 '19 at 20:12
  • Quick thought: is .do not preferred to .subscribe(onNext:) in this context as we're performing a UI side effect rather than altering the elements within the stream? The manual update of the UI is more compact than the other versions (even with the extra line) and just as expressive. I've now got the expected behaviour. Thanks again – rustproofFish May 31 '19 at 20:19
  • as for me I'm ok with both: ```swift viewModel.addObservation.executionObservables .do(onNext: { _ in self.observationTextField = "" self.addObservationButton.isEnabled = false }) .subscribe() .disposed(by: disposeBag) ``` and ```swift viewModel.addObservation.executionObservables .subscribe({ [unowned self] _ in self.observationTextField = "" self.addObservationButton.isEnabled = false }) .disposed(by: disposeBag) ``` Personally I prefer code in subscribe closure. – Sergey Duhovich Jun 01 '19 at 22:42