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.