PrepareFor
PrepareFor

Reputation: 2598

How to trigger textField(_:shouldChangeCharactersIn:replacementString:) method in iOS

I'm trying to make some features on UITextField which for input amount

Wrapper view contains this UITextField, and that view conforms UITextFieldDelegate

so i implement textField(_:shouldChangeCharactersIn:replacementString:) method in Wrapper view class

In that method, I manipulate the input text from keyboard(add comma or block input when amount exceed maximum input amount)

It does work well in system keyboard, but not in custom keyboard

In custom keyboard, i make buttons and if some button pressed, i set the text to textfield what i make (amount text field) like this

textField.text = someText

but set text to textfield explicitly, it doesn't trigger textField(_:shouldChangeCharactersIn:replacementString:) method

How can i trigger textField(_:shouldChangeCharactersIn:replacementString:) method when using custom keyboard

Upvotes: 0

Views: 1293

Answers (1)

DonMag
DonMag

Reputation: 77433

You cannot "trigger" shouldChangeCharactersIn, but there are various ways to approach this.

One method would be to move your "validation" code into its own function. You could then call that function both from shouldChangeCharactersIn and from your custom keyboard input.

So, for a very simple example - allowing only Numeric entry - it might look something like this:

func myShouldChangeCharacters(_ textField: UITextField, in range: NSRange, replacementString string: String) -> Bool {

    // whatever you want to do to validate the input

    if !"0123456789".contains(string) {
        return false
    }
    return true
}

// textField delegate call
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    return myShouldChangeCharacters(textField, in: range, replacementString: string)
}

// button tapped    
@objc func btnTapped(_ sender: Any) {
    // make sure we're
    //  called by a button
    //  get the button title
    //  convert textField's .selectedTextRange to a NSRange
    guard let btn = sender as? UIButton,
          let str = btn.currentTitle,
          let rng = myTextField.selectedRange
    else {
        return
    }
    if myShouldChangeCharacters(myTextField, in: rng, replacementString: str) {
        myTextField.insertText(str)
    }
}

uses this extension:

// helper extension to convert
//  text field's selectedTextRange
//  to a NSRange
extension UITextInput {
    var selectedRange: NSRange? {
        guard let range = selectedTextRange else { return nil }
        let location = offset(from: beginningOfDocument, to: range.start)
        let length = offset(from: range.start, to: range.end)
        return NSRange(location: location, length: length)
    }
}

Here's a complete example that has a text field at the top, with 4 buttons below it labeled 1, 2, A, B, and only allows Numerics:

class ViewController: UIViewController, UITextFieldDelegate {

    let myTextField: UITextField = {
        let v = UITextField()
        v.borderStyle = .roundedRect
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let btnsStack: UIStackView = {
            let v = UIStackView()
            v.axis = .horizontal
            v.distribution = .fillEqually
            v.spacing = 40.0
            return v
        }()
        
        // create 4 buttons
        ["1", "2", "A", "B"].forEach { str in
            let b = UIButton()
            b.setTitle(str, for: [])
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.gray, for: .highlighted)
            b.backgroundColor = .systemRed
            b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
            btnsStack.addArrangedSubview(b)
        }
        
        [myTextField, btnsStack].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        // respect safe area
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            myTextField.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            myTextField.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            myTextField.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            btnsStack.topAnchor.constraint(equalTo: myTextField.bottomAnchor, constant: 20.0),
            btnsStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            btnsStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),

        ])
        
        myTextField.delegate = self
    }
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        return myShouldChangeCharacters(textField, in: range, replacementString: string)
    }
    
    @objc func btnTapped(_ sender: Any) {
        // make sure we're
        //  called by a button
        //  get the button title
        //  convert textField's .selectedTextRange to a NSRange
        guard let btn = sender as? UIButton,
              let str = btn.currentTitle,
              let rng = myTextField.selectedRange
        else {
            return
        }
        if myShouldChangeCharacters(myTextField, in: rng, replacementString: str) {
            myTextField.insertText(str)
        }
    }
    
    func myShouldChangeCharacters(_ textField: UITextField, in range: NSRange, replacementString string: String) -> Bool {
        if !"0123456789".contains(string) {
            return false
        }
        return true
    }
    
}

// helper extension to convert
//  text field's selectedTextRange
//  to a NSRange
extension UITextInput {
    var selectedRange: NSRange? {
        guard let range = selectedTextRange else { return nil }
        let location = offset(from: beginningOfDocument, to: range.start)
        let length = offset(from: range.start, to: range.end)
        return NSRange(location: location, length: length)
    }
}

Upvotes: 1

Related Questions