95

I'm using shouldChangeCharactersInRange as a way of using on-the-fly type search.

However I'm having a problem, shouldChangeCharactersInRange gets called before the text field actually updates:

In Objective C, I solved this using using below:

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSString * searchStr = [textField.text stringByReplacingCharactersInRange:range withString:string];

    return YES;
}

However, I've tried writing this in Swift:

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {
    let txtAfterUpdate:NSString = self.projectSearchTxtFld.text as NSString
    txtAfterUpdate.stringByReplacingCharactersInRange(range, withString: string)

    self.callMyMethod(txtAfterUpdate)
    return true
}

The method still gets called before I get a value?

János
  • 32,867
  • 38
  • 193
  • 353
Fudgey
  • 3,793
  • 7
  • 32
  • 53

10 Answers10

212

Swift 4, Swift 5

This method doesn't use NSString

// MARK: - UITextFieldDelegate

extension MyViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField,
                   shouldChangeCharactersIn range: NSRange,
                   replacementString string: String) -> Bool {
        if let text = textField.text,
           let textRange = Range(range, in: text) {
           let updatedText = text.replacingCharacters(in: textRange,
                                                       with: string)
           myvalidator(text: updatedText)
        }
        return true
    }
}

Note. Be careful when you use a secured text field.

Vyacheslav
  • 26,359
  • 19
  • 112
  • 194
81

stringByReplacingCharactersInRange return a new string, so how about:

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {
    if let text = textField.text as NSString? {
        let txtAfterUpdate = text.replacingCharacters(in: range, with: string)
        self.callMyMethod(txtAfterUpdate)
    }
    return true
}
mbelsky
  • 6,093
  • 2
  • 26
  • 34
Mike Pollard
  • 10,195
  • 2
  • 37
  • 46
  • 4
    That's curious that we have to convert to NSString while using with Swift string it doesn't work due to different argument types. – Hola Soy Edu Feliz Navidad Apr 28 '15 at 04:53
  • any way without using the old `NSString`? – Martin Jan 23 '17 at 14:05
  • @Martin I don't think there's one that isn't arduously painful in comparison. NSString <> String conversions are far from the worst though, given they are toll-free bridged – bitwit Apr 26 '17 at 23:56
  • It does not take more than 2 characters at a time – Nupur Sharma Jun 19 '17 at 05:37
  • 2
    Not work in few language such as Korean. Korean word is not completed with just appending a last character. In case of typing "대한민국", txtAfterUpdate would be "대한민구ㄱ". FYI for Koreans. – Mark Sep 15 '17 at 09:53
  • Does not work when a password protected field is about to be cleared with a single backspace – Daniel Mar 23 '20 at 16:47
44

Swift 3 & 4

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    let textFieldText: NSString = (textField.text ?? "") as NSString
    let txtAfterUpdate = textFieldText.replacingCharacters(in: range, with: string)
    callMyMethod(txtAfterUpdate)

    return true
}

func textFieldShouldClear(_ textField: UITextField) -> Bool {
    callMyMethod("")
    return true
}

Swift 2.2

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let textFieldText: NSString = textField.text ?? ""
    let txtAfterUpdate = textFieldText.stringByReplacingCharactersInRange(range, withString: string)
    callMyMethod(txtAfterUpdate)

    return true
}

func textFieldShouldClear(textField: UITextField) -> Bool {
    callMyMethod("")
    return true
}

Though the textField.text property is an optional, it cannot be set to nil. Setting it to nil is changed to empty string within UITextField. In the code above, that is why textFieldText is set to empty string if textField.text is nil (via the nil coalescing operator ??).

Implementing textFieldShouldClear(_:) handles the case where the text field's clear button is visible and tapped.

Mobile Dan
  • 6,444
  • 1
  • 44
  • 44
12

In Swift 3 it would look like this:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    let text: NSString = (textField.text ?? "") as NSString
    let resultString = text.replacingCharacters(in: range, with: string)

    return true
}
Andrey Gordeev
  • 30,606
  • 13
  • 135
  • 162
2

shouldChangeCharactersIn is called on every key press.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    // get the current text, or use an empty string if that failed
    let currentText = textField.text ?? ""

    // attempt to read the range they are trying to change, or exit if we can't
    guard let stringRange = Range(range, in: currentText) else { return false }

    // add their new text to the existing text
    let updatedText = currentText.replacingCharacters(in: stringRange, with: string)

    // make sure the result is under 16 characters
    return updatedText.count <= 16
}
aheze
  • 24,434
  • 8
  • 68
  • 125
  • 4
    This seems to be copied entirely from https://www.hackingwithswift.com/example-code/uikit/how-to-limit-the-number-of-characters-in-a-uitextfield-or-uitextview – aheze Aug 23 '21 at 23:21
1

shouldChangeCharactersInRange

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool { }

This function is called when changes are made but UI is not updated and waiting for your choice

Take a look at returned bool value

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
  • If you return true - it means that iOS accept changes(text, caret...)
  • If you return false - it means that you are responsible for all this stuff
yoAlex5
  • 29,217
  • 8
  • 193
  • 205
0

Swift 3


If you want to pre-process the characters the user typed or pasted, the following solution workes like a charm

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

    let strippedString = <change replacements string so it fits your requirement - strip, trim, etc>

    // replace current content with stripped content
    if let replaceStart = textField.position(from: textField.beginningOfDocument, offset: range.location),
        let replaceEnd = textField.position(from: replaceStart, offset: range.length),
        let textRange = textField.textRange(from: replaceStart, to: replaceEnd) {

        textField.replace(textRange, withText: strippedString)
    }
    return false
}

Find it here: https://gist.github.com/Blackjacx/2198d86442ec9b9b05c0801f4e392047

blackjacx
  • 9,011
  • 7
  • 45
  • 56
  • wouldn't this overwrite existing characters instead of inserting them? i still need to try this out, but afaik the `shouldChangeCharactersIn` method is called _before_ the textField content is changed – fabb May 30 '18 at 10:32
  • Yes that is true, but you already can construct the new string that is set to the textfield when you return `true` in this function. Basically you can do what you want with the current and the new text field content plus you can control if you want to reject the change plus plus you can modify the string that is set. So you have full control here. – blackjacx Jun 04 '18 at 08:07
0

This is essentially @Vyacheslav's answer independently arrived at for my own use case, just in case the stylistic approach resonates :-)

func textField(_ textField: UITextField, shouldChangeCharactersIn nsRange: NSRange, replacementString: String) -> Bool {
    let range = Range(nsRange, in: textField.text!)!
    let textWouldBecome =  textField.text!.replacingCharacters(in: range, with: replacementString)
    if textWouldBecome != eventModel.title {
        self.navigationItem.setHidesBackButton(true, animated: true)
    } else {
        self.navigationItem.setHidesBackButton(false, animated: true)
    }
    return true
}

Replace eventModel.title with whatever you're checking for the change against obviously.

clearlight
  • 12,255
  • 11
  • 57
  • 75
-1

Swift 13

func textFieldDidChangeSelection(_ textField: UITextField) {
    DispatchQueue.main.async { 
        self.textBinding.wrappedValue = textField.text ?? ""
    }
}
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 14 '23 at 14:58
  • `DispatchQueue.main.async` is a pain for new <-> intermediate Swift coders... this answer won't be helpful unless you explain why this solves the problem. – benc Mar 15 '23 at 06:53
-7

To get the exact text in the my UITextField component in Swift 3.0 I used:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
   let enteredTxt = textField.text! + string
   doSomethingWithTxt(enteredTxt) //some custom method
}
Pavle Mijatovic
  • 773
  • 10
  • 6