2

I have a textfield to validate, I want to disable a button whenever user is typing. After user stops typing (debounce by 1 second), the validation is carried out and the button is conditionally enabled based on the result. Notice the corner case when user typed only one character, validation should still happen.

--"a"-"ab"-"abc"------------------"ab"--"a"------------------"ab"-----------------

--false---------validate("abc")---false----validate("a")-----false--validate("ab")

This SO (Deliver the first item immediately, 'debounce' following items) proposes the following solution in RxJava. But it seems only returns the very first element, not when user starts typing again after debounce? Correct me if I am wrong

Observable.from(items).publish(publishedItems -> 
    publishedItems.limit(1).concatWith(
        publishedItems.skip(1).debounce(1, TimeUnit.SECONDS)
    )
)
woodcutting
  • 271
  • 1
  • 9
Jack Guo
  • 3,959
  • 8
  • 39
  • 60
  • Is it correct to say that you want the button to be disabled whenever the user is typing, and when they are complete, to match the result of the validation function? – woodcutting Jul 15 '18 at 01:00
  • Exactly, whether they have completed typing is determined by `debounce` – Jack Guo Jul 15 '18 at 01:05

2 Answers2

1

One option is to separate the question "is the user typing" from "is the input valid."

One (somewhat clumsy) way of describing whether the user is typing or not:

let isTyping = PublishSubject<Bool>()

textField.rx.text
    .map { _ in true }
    .bind(to: isTyping)
    .disposed(by: disposeBag)

textField.rx.text
    .debounce(1.0, scheduler: MainScheduler.instance)
    .map { _ in false}
    .bind(to: isTyping)
    .disposed(by: disposeBag)

Then, you could describe the button enabled state like so:

isTyping.withLatestFrom(isValid) { !$0 && $1 }
            .bind(to: button.rx.isEnabled)

Of course for this approach to have an effect as soon as the user starts typing, it would be best for isValid to begin with a value.

You could also simplify this in the following manner. If these values are only used locally, this may be fine.

textField.rx.text
    .map { _ in false }
    .bind(to: button.rx.isEnabled)
    .disposed(by: disposeBag)

textField.rx.text
    .debounce(1.0, scheduler: MainScheduler.instance)
    .flatMap { [weak self] in self?.validate($0) }
    .bind(to: button.rx.isEnabled)
    .disposed(by: disposeBag)

Note that with this approach, asynchronous validation operations would need to be canceled when the user resumes typing.

Hopefully, someone can suggest a cleaner solution, but I think this is a decent place to start.

woodcutting
  • 271
  • 1
  • 9
  • I actually thought about the second approach as well. Is there a reactive way to cancel the async operation or ignore its result once user is typing again? – Jack Guo Jul 15 '18 at 05:15
  • It's hard for me to answer this without knowing more about your application's network layer. The "sledgehammer" option for restarting observation in general is to reallocate the associated dispose bag, but I don't think that's what you want here. One other option is to check that the textField hasn't changed during the course of validation: – woodcutting Jul 15 '18 at 10:02
1

After some thinking, I was able to fully solve it in the following way, everything works as I intended

    let input = textField.rx.text.distinctUntilChanged()
    let keystroke = input.map { _ in Observable.just(false) }
    let validate = input
        .flatMap { Observable.from(optional: $0) }  // 1  
        .filter { $0.count >= minimumTextLength }      
        .debounce(1, scheduler: MainScheduler.instance)
        .map { self.networkManager.validate($0).asObservable() } // 2
    return Observable.merge(keystroke, validate).switchLatest().distinctUntilChanged() // 3
  1. Safely unwrap optional strings
  2. validate returns Single<Bool> in my case, so I turn it into Observable. Notice I intentionally used map not flatMap in order to use the functionality switchLatest provides in step 3
  3. I merge a and b to create an Observable<Observable<Bool>>, switchLatest allows me to ignore the validate result if user starts typing again. distinctUntilChanged discards repeated falses
Jack Guo
  • 3,959
  • 8
  • 39
  • 60