10

I want use UITextfield with RxSwift. My goal is allowing/not input character in User keyboard and removing character from copy paste, I need handle UITextfield's delegate "shouldChangeCharactersInRange" with RxSwift.

How to implement with RxSwift?

I am using RxSwift version 4. Case 1: Input from keyboard: A123 Process from RxSwift : Accept 123 (not allowing NumberPad) Output : 123

Case 2: Input form Copy Paste from Contacts: \U202d1111111111\U202c Process from RxSwift : remove all control character, accept 1111111111 Output: 1111111111

If in general we can use shouldChangeCharactersInRange , but how to use with RxSwift?

duan
  • 8,515
  • 3
  • 48
  • 70
David Sujarwadi
  • 205
  • 2
  • 10
  • 1
    Maybe this can help you...https://stackoverflow.com/questions/39627440/observing-uitextfield-editing-with-rxswift – davebcn87 May 28 '18 at 09:43
  • Please rephrase your question. Also, tell us what you have in `textField(textField:shouldChangeCharactersIn range:replacementString:)` already. – staticVoidMan May 29 '18 at 11:44

2 Answers2

12

In general, you should not be mutating state in shouldChangeCharactersInRange, even if you aren't using Rx. That callback is a query not a command. The textfield is merely asking you if it should perform the default behavior, not telling you to update it. The behavior you are trying to implement should be in the editingChanged action.

Since you are using Rx, the text field's rx.text observer is equivalent to the editingChanged action and should be used instead. The hardest part of the procedure is making sure you don't loose the user's place if they are inserting/deleting in the middle of the string.

In your viewDidLoad:

textField.rx.text.orEmpty
    .map(digitsOnly)
    .subscribe(onNext: setPreservingCursor(on: textField))
    .disposed(by: bag)

Supporting global functions:

func digitsOnly(_ text: String) -> String {
    return text.components(separatedBy: CharacterSet.decimalDigits.inverted).joined(separator: "")
}

func setPreservingCursor(on textField: UITextField) -> (_ newText: String) -> Void {
    return { newText in
        let cursorPosition = textField.offset(from: textField.beginningOfDocument, to: textField.selectedTextRange!.start) + newText.count - (textField.text?.count ?? 0)
        textField.text = newText
        if let newPosition = textField.position(from: textField.beginningOfDocument, offset: cursorPosition) {
            textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
        }
    }
}

BTW, even if you are presenting the number pad keyboard, you still need some code like this because the user might have a bluetooth keyboard hooked up and thus could still enter non-numbers.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • 1
    Note that `textField.text = newText` will not cause an event of rx.text controlProperty. If you want to force a text event you need to manually send valueChanged event`textField.sendActions(for: .valueChanged)` – mas'an Feb 06 '19 at 13:04
  • If you want to force a text event in this situation, you are treating your view as if it was part of your model. Don't do that. – Daniel T. Feb 06 '19 at 13:08
0

You can observe text update and revert it when necessary:

Observable.zip(textfield.rx.text, textfield.rx.text.skip(1))
    .subscribe(onNext: { (old, new) in
        if $invalid {
            textfield.text = old
        }
    })
duan
  • 8,515
  • 3
  • 48
  • 70
  • .skip only neglects a number of objects emitted first, then it starts receiving current objects, so the new and old values will be the same. However, in this case, you zip two observables that emit at the same time so it may give new as old and vice versa, depends on which fires first – Denis Litvin Aug 12 '18 at 19:17