Why is replacingCharacters necessary for changing the text in text field? - ios

I can change my way to present numbers in the text field using the following line of code based on what I type in the text field. But I can't even type in numbers in the second version of my code.
Why
[let oldText = textField.text! as NSString
var newText = oldText.replacingCharacters(in: range, with: string)]
is necessary?
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let oldText = textField.text! as NSString
var newText = oldText.replacingCharacters(in: range, with: string)
var newTextString = String(newText)
let digits = CharacterSet.decimalDigits
var digitText = ""
for c in (newTextString?.unicodeScalars)! {
if digits.contains(UnicodeScalar(c.value)!) {
digitText.append("\(c)")
}
}
// Format the new string
if let numOfPennies = Int(digitText) {
newText = "$" + self.dollarStringFromInt(numOfPennies) + "." + self.centsStringFromInt(numOfPennies)
} else {
newText = "$0.00"
}
textField.text = newText
return false
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField.text!.isEmpty {
textField.text = "$0.00"
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true;
}
func dollarStringFromInt(_ value: Int) -> String {
return String(value / 100)
}
func centsStringFromInt(_ value: Int) -> String {
let cents = value % 100
var centsString = String(cents)
if cents < 10 {
centsString = "0" + centsString
}
return centsString
}
If I change it like this, it doesn't work anymore.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var newText = textField.text
let digits = CharacterSet.decimalDigits
var digitText = ""
for c in (newText?.unicodeScalars)! {
if digits.contains(UnicodeScalar(c.value)!) {
digitText.append("\(c)")
}
}
// Format the new string
if let numOfPennies = Int(digitText) {
newText = "$" + self.dollarStringFromInt(numOfPennies) + "." + self.centsStringFromInt(numOfPennies)
} else {
newText = "$0.00"
}
textField.text = newText
return false
}

textField(_:shouldChangeCharactersIn:replacementString:) gets called before the text is replaced inside the text field. This method is used for getting the new text before it's accessible with textField.text.
If you just want to run logic after the replacement happened, observe the notification called .UITextFieldTextDidChange.
NotificationCenter.default.addObserver(self, selector: #selector(textFieldTextDidChange), name: .UITextFieldTextDidChange, object: textField)
func textFieldTextDidChange() {
print(textField.text)
}

"shouldChangeCharactersIn" is called BEFORE the text field is modified. Since your code always returns false, whatever the user has typed in (or pasted in) will not update the field's content unless you do it yourself.
In the second version you are completely ignoring the user's input so your field's value never changes.

Lets answer by part.
1) The method replacingCharacters(in: range, with: string)isn't required. This methods will replace the substring that matches with range specified. So, according with Apple docs:
stringByReplacingCharactersInRange:withString:
Returns a new string in which the characters in a specified range of the receiver are replaced by a given string
2) The real reason the code change don't work is the return value false. Try return true and the change will work. Whenever you set the text field text property, the method textField:shouldChangeCharactersIn:replacementString: will be invoked (since its delegate is assigned). Returning false you are telling the program it should not replace the characters.
UPDATE
Try use the UIControl event Value Changed as an #IBAction instead textField:shouldChangeCharactersIn:replacementString: this will work well.
Obs: Sorry for my english

Related

How to allow to type only one sentence (word or chars) in TextField? Swift 5

Hello everyone!) Need some help!)
How can I allow the user to enter only one sentence (word or characters) in a TextField with some rules?)
The user must enter only this word:
Qwerty
Then text field must shows automatically hyphen:
Qwerty-
And after that the user can type in text field only this digits:
12345
Expected result must be only this:
Qwerty-12345
The order of entering each letter or number is very important!)
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = onlyCharTextField.text else { return true }
if textField == onlyCharTextField {
let allowedCharacters = "Q-w-e-r-t-y "
let allowedCharacterSet = CharacterSet(charactersIn: allowedCharacters)
let allowedDigits = "1-2-3-4-5"
let allowedDigitSet = CharacterSet(charactersIn: allowedDigits)
if text == "Qwerty" {
onlyCharTextField.text = "Qwerty" + "-"
}
let typedCharacterSet = CharacterSet(charactersIn: string)
let alphabet = allowedCharacterSet.isSuperset(of: typedCharacterSet) || allowedDigitSet.isSuperset(of: typedCharacterSet)
return alphabet
} else {
return false
}
}
I'm confused..(( Do you have any ideas how to implement this?)
Thanks for every answer!)
It's not that complicated. You can use the "first compute what the new text would be" pattern, and then check if "Qwerty-12345" starts with that text. This is because if the text is entered in the correct order, then the text is always a part (or the entirety) of the beginning of "Qwerty-12345":
Q
Qw
Qwe
Qwer
Qwert
Qwerty-
Qwerty-1
Qwerty-12
Qwerty-123
Qwerty-1234
Qwerty-12345
A special case is when the text is Qwerty. This is when you don't allow the keyboard to change the text, and instead change it programmatically to Qwerty-.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let text = (textField.text ?? "") as NSString
let newText = text.replacingCharacters(in: range, with: string)
if newText == "Qwerty" {
textField.text = "Qwerty-"
return false
}
return "Qwerty-12345".starts(with: newText)
}
Note that this still allows the user to delete what they have already entered, but only from the end, and enter Qwerty-12345 from the deleted point. If you want to disallow that, you can check if the replacementString parameter is empty, which is indicative of a deletion:
if string.isEmpty {
return false
}
This doesn't disable pasting. If you want that, see How to disable pasting in a TextField in Swift?
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let expectedText = "Qwerty-12345" //Text u want user to enter
let halfText = "Qwerty" //special case where u enter an additional -
let enteredText = "\(textField.text ?? "")\(string)" //get the text user has entered
if enteredText == expectedText.prefix(enteredText.count){ //check if the user entered text matches to the the first part of your string
// if it matches change the text of the text field
if enteredText == halfText{
textField.text = "\(enteredText)-" //special case where u add an -
}
else{
textField.text = enteredText
}
}
return false
}

Textfield is not formatted once populated with data

I have a textfield for numbers that is already formatted when user types in the field; it would look like this:
Here is my code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
phoneNumberTextField.delegate = self
retrieveUserData()
}
func formattedNumber(number: String) -> String {
let cleanPhoneNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let mask = "(XXX) XXX-XXXX"
var result = ""
var index = cleanPhoneNumber.startIndex
for ch in mask where index < cleanPhoneNumber.endIndex {
if ch == "X" {
result.append(cleanPhoneNumber[index])
index = cleanPhoneNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
extension AccounInfotVC: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return false }
let newString = (text as NSString).replacingCharacters(in: range, with: string)
textField.text = formattedNumber(number: newString)
return false
}
}
However, when my view loads; the number is not formatted once its auto populated from the database with this code:
func retrieveUserData(){
db.collection(DatabaseRef.Users).document(userID).getDocument { (snap, error) in
self.phoneNumberTextField?.text = phoneNumber
}
}
so the number gets populated like this:
instead of this:
Any idea how i can get it to populate the textfield in the same format as when user types?
Note that, when I save the phone number, I remove spaces and the characters so number is saved like 1234567889 in the database.
This
self.phoneNumberTextField?.text = phoneNumber
won't trigger shouldChangeCharactersIn , you need
self.phoneNumberTextField?.text = formattedNumber(number:phoneNumber)

Show "#" instead of "bullets" in UITextField for "Secure Text Entry"

I have a requirement to show "#" instead of bullets for password field.
But as there is no default option available for it in UITextField.
I have tried to write custom logic in "shouldChangeCharactersInRange"
But i am not able to handle the index when user will remove or add any specific character from in-between.
So here are my questions :-
1. Do i need to find any library
2. There is any other default option available for it?
3. Need to write custom logic for it? If so where i can handle it correctly "shouldChangeCharactersInRange" or "textFieldDidChange"
No you dont need to find any 3rd party library for this logic
No there is no default option available for your need
Yes, you need to write a custom logic for your demand, So here it goes...
var passwordText = String()
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == textFieldPassword {
var hashPassword = String()
let newChar = string.characters.first
let offsetToUpdate = passwordText.index(passwordText.startIndex, offsetBy: range.location)
if string == "" {
passwordText.remove(at: offsetToUpdate)
return true
}
else { passwordText.insert(newChar!, at: offsetToUpdate) }
for _ in passwordText.characters { hashPassword += "#" }
textField.text = hashPassword
return false
}
Swift 4:-
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == textFieldPassword {
var hashPassword = String()
let newChar = string.first
let offsetToUpdate = passwordText.index(passwordText.startIndex, offsetBy: range.location)
if string == "" {
passwordText.remove(at: offsetToUpdate)
return true
}
else { passwordText.insert(newChar!, at: offsetToUpdate) }
for _ in 0..<passwordText.count { hashPassword += "#" }
textField.text = hashPassword
return false
}
return true
}
Use a normal textfield without the secure input option. When a user enters a character, save it to a string variable, and replace it in the textfield with the character you wish to present instead of the bullets.
class ViewController: UIViewController,UITextFieldDelegate {
let textField = UITextField(frame :CGRect(x:16,y:50,width:200,height: 40))
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
self.view.addSubview(textField)
textField.becomeFirstResponder()
}
var password: String = ""
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool{
password = password+string
textField.text = textField.text!+"#"//Character you want
print("\(password)")
return false
}
}
This is in Swift 2. Hope it Helps!!
Improved Mr. Bean's answer in swift 5. To fix Copy&Paste bugs.
var passNSString : NSString = ""
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var hashPassword = String()
passNSString = passNSString.replacingCharacters(in: range, with: string) as NSString
for _ in 0..<passNSString.length { hashPassword += "#" }
textField.text = hashPassword
print("str", passNSString)
return false
}

Swift 3: Append to UITextField while Editing Changed

Probably a simple one although no answers updated for Swift 3.
How can I append characters to a UITextField while the textfield is being edited? The characters should be appended while the user types not after the editing ends.
For example:
User types: 1
Field Updates: 1 kg
User types 123
Field Updates: 123 kg
Was trying to tackle this using EditingChanged IBActions but how can I stop the value from appending "kg" for each new character that is typed?
Example "1 kg 2 kg 3 kg"
Try this way it may help you out.
Add target for textfield on text is being editing
textField.addTarget(self, action: #selector(self.textFieldDidChange), for: .editingChanged)
In Observer method try the following way
func textFieldDidChange(textfield: UITextField) {
var text = textField.text?.replacingOccurrences(of: " KG", with: "")
text = text! + " KG"
textField.text = text
print("Text changed")
}
You want the UITextFieldDelegate method
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
I should warn you that this is incredibly irritating to implement because of the way Swift handles Strings. Its usually better to just cast the text to NSString (which is UTF16) and deal with the range that way. However if you are just doing numbers and can live with a fixed decimal place the case is much easier to handle. Keep a custom number that represents your "real number" and just update the field to reflect your formatted number. Since you only allow digits there are finitely many cases to handle (this code will not handle copy/paste).
You must set the textfield delegate and keyboard (to numeric) in the storyboard.
class ViewController: UIViewController{
#IBOutlet weak var textField: UITextField!
fileprivate let digits: Set<String> = ["0","1","2","3","4","5","6","7","8","9"]
fileprivate let decimalPlaces = 2
fileprivate let suffix = " kg"
fileprivate lazy var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = self.decimalPlaces
formatter.maximumFractionDigits = self.decimalPlaces
formatter.locale = NSLocale.current
return formatter
}()
fileprivate var amount: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if digits.contains(string) {
amount *= 10
amount += Int(string)!
} else if string == "" {
amount /= 10
}
guard amount > 0 else {
textField.text = ""
return false
}
let digitsAfterDecimal = formatter.maximumFractionDigits
var value = Double(amount)
for _ in 0..<digitsAfterDecimal {
value /= 10
}
textField.text = formatter.string(from: NSNumber(value: value))! + suffix
return false
}
}
Conform the textfield delegate to the controller and try this solution.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string == "" {
// handle backspace scenario here
return true
} else if var textString = textField.text {
let unitString = "kg"
if textString.contains(unitString) {
textString = textString.replacingOccurrences(of: unitString, with: "")
textString += string + unitString
textField.text = textString
} else {
textField.text = string + unitString
}
return false
}
return true
}
Use UITextFieldDelegate's function:
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool // return NO to not change text
You'll have to combine the text from the textfield.text and string parameter from this function using the range parameter to get the string To Be formed on textfield while you type/paste/clear based on range.
Keep track on range.length as it always gives the count of string being deleted/cleared and is 0 when you enter text. And range.location gives the position of edit.
Then you can set the formed string to textfield.text and return false Remember - return false and not true

Set the maximum character length of a UITextField in Swift

I know there are other topics on this, but I can't seem to find out how to implement it.
I'm trying to limit a UITextField to only five characters.
Preferably alphanumeric, -, ., and _.
I've seen this code:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool
{
let maxLength = 4
let currentString: NSString = textField.text
let newString: NSString =
currentString.stringByReplacingCharactersInRange(range, withString: string)
return newString.length <= maxLength
}
and
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let length = count(textField.text.utf16) + count(string.utf16) - range.length
return length <= 10
}
How can I actually implement it? Which "textfield" should I swap out for my custom named UITextField?
Your view controller should conform to UITextFieldDelegate, like below:
class MyViewController: UIViewController, UITextFieldDelegate {
}
Set the delegate of your textfield: myTextField.delegate = self
Implement the method in your view controller:
textField(_:shouldChangeCharactersInRange:replacementString:)
All together:
class MyViewController: UIViewController, UITextFieldDelegate // Set delegate to class
#IBOutlet var mytextField: UITextField // textfield variable
override func viewDidLoad() {
super.viewDidLoad()
mytextField.delegate = self // set delegate
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool
{
let maxLength = 4
let currentString: NSString = textField.text
let newString: NSString = currentString.stringByReplacingCharactersInRange(range, withString: string)
return newString.length <= maxLength
}
For Swift 4
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let maxLength = 1
let currentString: NSString = (textField.text ?? "") as NSString
let newString: NSString = currentString.replacingCharacters(in: range, with: string) as NSString
return newString.length <= maxLength
}
For Swift 5
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let maxLength = 1
let currentString = (textField.text ?? "") as NSString
let newString = currentString.replacingCharacters(in: range, with: string)
return newString.count <= maxLength
}
Allowing only a specified set of characters to be entered into a given text field
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
var result = true
if mytextField == textField {
if count(string) > 0 {
let disallowedCharacterSet = NSCharacterSet(charactersInString: "0123456789.-").invertedSet
let replacementStringIsLegal = string.rangeOfCharacterFromSet(disallowedCharacterSet) == nil
result = replacementStringIsLegal
}
}
return result
}
How to program an iOS text field that takes only numeric input with a maximum length
Modern Swift
Note that a lot of the example code online is extremely out of date.
Paste the following into any Swift file in your project, example "Handy.swift".
This fixes one of the silliest problems in iOS:
Your text fields now have a .maxLength.
It is completely OK to set that value in storyboard or set in code while the app is running.
// Handy.swift
import UIKit
private var __maxLengths = [UITextField: Int]()
extension UITextField {
#IBInspectable var maxLength: Int {
get {
guard let l = __maxLengths[self] else {
return 150 // (global default-limit. or just, Int.max)
}
return l
}
set {
__maxLengths[self] = newValue
addTarget(self, action: #selector(fix), for: .editingChanged)
}
}
func fix(textField: UITextField) {
let t = textField.text
textField.text = t?.prefix(maxLength).string
}
}
It's that simple.
An even simpler one-off version...
The above fixes all text fields in the whole project.
If you just want one particular text field to simply be limited to say "4", and that's that...
class PinCodeEntry: UITextField {
override func didMoveToSuperview() {
super.didMoveToSuperview()
addTarget(self, action: #selector(fixMe), for: .editingChanged)
}
#objc private func fixMe() { text = text?.prefix(4) }
}
That's all there is to it.
(Here's a similar very useful tip relating to UITextView,
https://stackoverflow.com/a/42333832/294884 )
In Swift 4, simply use:
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return range.location < 10
}
The same way Steven Schmatz did it but using Swift 3.0 :
//max Length
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool
{
let maxLength = 4
let currentString: NSString = textField.text! as NSString
let newString: NSString = currentString.replacingCharacters(in: range, with: string) as NSString
return newString.length <= maxLength
}
Simple solution without using a delegate:
TEXT_FIELD.addTarget(self, action: #selector(editingChanged(sender:)), for: .editingChanged)
#objc private func editingChanged(sender: UITextField) {
if let text = sender.text, text.count >= MAX_LENGHT {
sender.text = String(text.dropLast(text.count - MAX_LENGHT))
return
}
}
For Swift 5:
Just write one line to set the maximum character length:
self.textField.maxLength = 10
For more details, see Max character limit of UITextField and allowed characters Swift. (Also credited.)
I think an extension is more handy for this. See the full answer here.
private var maxLengths = [UITextField: Int]()
// 2
extension UITextField {
// 3
#IBInspectable var maxLength: Int {
get {
// 4
guard let length = maxLengths[self] else {
return Int.max
}
return length
}
set {
maxLengths[self] = newValue
// 5
addTarget(
self,
action: #selector(limitLength),
forControlEvents: UIControlEvents.EditingChanged
)
}
}
func limitLength(textField: UITextField) {
// 6
guard let prospectiveText = textField.text
where prospectiveText.characters.count > maxLength else {
return
}
let selection = selectedTextRange
// 7
text = prospectiveText.substringWithRange(
Range<String.Index>(prospectiveText.startIndex ..< prospectiveText.startIndex.advancedBy(maxLength))
)
selectedTextRange = selection
}
}
My Swift 4 version of shouldChangeCharactersIn
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
guard let preText = textField.text as NSString?,
preText.replacingCharacters(in: range, with: string).count <= MAX_TEXT_LENGTH else {
return false
}
return true
}
Other solutions posted previously produce a retain cycle due to the textfield map. Besides, the maxLength property should be nullable if not set instead of artificial Int.max constructions; and the target will be set multiple times if maxLength is changed.
Here an updated solution for Swift4 with a weak map to prevent memory leaks and the other fixes
private var maxLengths = NSMapTable<UITextField, NSNumber>(keyOptions: NSPointerFunctions.Options.weakMemory, valueOptions: NSPointerFunctions.Options.strongMemory)
extension UITextField {
var maxLength: Int? {
get {
return maxLengths.object(forKey: self)?.intValue
}
set {
removeTarget(self, action: #selector(limitLength), for: .editingChanged)
if let newValue = newValue {
maxLengths.setObject(NSNumber(value: newValue), forKey: self)
addTarget(self, action: #selector(limitLength), for: .editingChanged)
} else {
maxLengths.removeObject(forKey: self)
}
}
}
#IBInspectable var maxLengthInspectable: Int {
get {
return maxLength ?? Int.max
}
set {
maxLength = newValue
}
}
#objc private func limitLength(_ textField: UITextField) {
guard let maxLength = maxLength, let prospectiveText = textField.text, prospectiveText.count > maxLength else {
return
}
let selection = selectedTextRange
text = String(prospectiveText[..<prospectiveText.index(from: maxLength)])
selectedTextRange = selection
}
}
I give a supplementary answer based on #Frouo. I think his answer is the most beautiful way. Because it's a common control we can reuse. And there isn't any leak problem here.
private var kAssociationKeyMaxLength: Int = 0
extension UITextField {
#IBInspectable var maxLength: Int {
get {
if let length = objc_getAssociatedObject(self, &kAssociationKeyMaxLength) as? Int {
return length
} else {
return Int.max
}
}
set {
objc_setAssociatedObject(self, &kAssociationKeyMaxLength, newValue, .OBJC_ASSOCIATION_RETAIN)
self.addTarget(self, action: #selector(checkMaxLength), for: .editingChanged)
}
}
// The method is used to cancel the check when using
// the Chinese Pinyin input method.
// Becuase the alphabet also appears in the textfield
// when inputting, we should cancel the check.
func isInputMethod() -> Bool {
if let positionRange = self.markedTextRange {
if let _ = self.position(from: positionRange.start, offset: 0) {
return true
}
}
return false
}
func checkMaxLength(textField: UITextField) {
guard !self.isInputMethod(), let prospectiveText = self.text,
prospectiveText.count > maxLength
else {
return
}
let selection = selectedTextRange
let maxCharIndex = prospectiveText.index(prospectiveText.startIndex, offsetBy: maxLength)
text = prospectiveText.substring(to: maxCharIndex)
selectedTextRange = selection
}
}
Simply just check with the number of characters in the string
Add a delegate to view controller and assign the delegate
class YorsClassName : UITextFieldDelegate {
}
Check the number of characters allowed for the text field
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField.text?.count == 1 {
return false
}
return true
}
Note: Here I checked for only characters allowed in textField.
TextField Limit Character After Block the Text in Swift 4
func textField(_ textField: UITextField, shouldChangeCharactersIn range:
NSRange,replacementString string: String) -> Bool
{
if textField == self.txtDescription {
let maxLength = 200
let currentString: NSString = textField.text! as NSString
let newString: NSString = currentString.replacingCharacters(in: range, with: string) as NSString
return newString.length <= maxLength
}
return true
}
I have something to add to Alaeddine's answer:
Your view controller should conform to UITextFieldDelegate
class MyViewController: UIViewController, UITextViewDelegate {
}
Set the delegate of your textfield:
To set the delegate, you can control drag from the textfield to your view controller in the storyboard. I think this is preferable to setting it in code
Implement the method in your view controller:
textField(_:shouldChangeCharactersInRange:replacementString:)
Update for Fattie's answer:
extension UITextField {
// Runtime key
private struct AssociatedKeys {
// Maximum length key
static var maxlength: UInt8 = 0
// Temporary string key
static var tempString: UInt8 = 0
}
// Limit the maximum input length of the textfiled
#IBInspectable var maxLength: Int {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.maxlength) as? Int ?? 0
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.maxlength, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
addTarget(self, action: #selector(handleEditingChanged(textField:)), for: .editingChanged)
}
}
// Temporary string
private var tempString: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.tempString) as? String
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.tempString, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
// When the text changes, process the amount of text in the input
// box so that its length is within the controllable range.
#objc private func handleEditingChanged(textField: UITextField) {
// Special processing for the Chinese input method
guard markedTextRange == nil else { return }
if textField.text?.count == maxLength {
// Set lastQualifiedString where text length == maximum length
tempString = textField.text
} else if textField.text?.count ?? 0 < maxLength {
// Clear lastQualifiedString when text length > maxlength
tempString = nil
}
// Keep the current text range in arcgives
let archivesEditRange: UITextRange?
if textField.text?.count ?? 0 > maxLength {
// If text length > maximum length, remove last range and to move to -1 postion.
let position = textField.position(from: safeTextPosition(selectedTextRange?.start), offset: -1) ?? textField.endOfDocument
archivesEditRange = textField.textRange(from: safeTextPosition(position), to: safeTextPosition(position))
} else {
// Just set current select text range
archivesEditRange = selectedTextRange
}
// Main handle string maximum length
textField.text = tempString ?? String((textField.text ?? "").prefix(maxLength))
// Last configuration edit text range
textField.selectedTextRange = archivesEditRange
}
// Get safe textPosition
private func safeTextPosition(_ optionlTextPosition: UITextPosition?) -> UITextPosition {
/* beginningOfDocument -> The end of the the text document. */
return optionlTextPosition ?? endOfDocument
}
}
Set the delegate of your textfield:
textField.delegate = self
Implement the method in your view controller:
// MARK: Text field delegate
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return range.location < maxLength (maxLength can be any maximum length you can define)
}
}
Here's a Swift 3.2+ alternative that avoids unnecessary string manipulation. In this case, the maximum length is 10:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let text = textField.text ?? ""
return text.count - range.length + string.count <= 10
}
This answer is for Swift 4 and is pretty straightforward with the ability to let backspace through.
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
return textField.text!.count < 10 || string == ""
}
This is working In Swift 4
Step 1: Set UITextFieldDelegate
class SignUPViewController: UIViewController , UITextFieldDelegate {
#IBOutlet weak var userMobileNoTextFiled: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
Step 2: Set the delegate
userMobileNoTextFiled.delegate = self // Set delegate
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// guard let text = userMobileNoTextFiled.text else { return true }
// let newLength = text.count + string.count - range.length
// return newLength <= 10
// }
Step 3: Call the function
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let maxLength = 10 // Set your need
let currentString: NSString = textField.text! as NSString
let newString: NSString =
currentString.replacingCharacters(in: range, with: string) as NSString
return newString.length <= maxLength
}
}
I use these steps. First set the delegate text field in viewdidload.
override func viewDidLoad() {
super.viewDidLoad()
textfield.delegate = self
}
And then shouldChangeCharactersIn after you include UITextFieldDelegate.
extension viewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let newLength = (textField.text?.utf16.count)! + string.utf16.count - range.length
if newLength <= 8 {
return true
}
else {
return false
}
}
}
Just in case, don't forget to guard the range size before applying it to the string. Otherwise, you will get a crash if the user will do this:
Type maximum length text
Insert something (nothing will be inserted due to the length limitation, but iOS doesn't know about it)
Undo insertion (you get a crash, because the range will be greater than the actual string size)
Also, using iOS 13 users can accidentally trigger this by gestures
I suggest you add to your project this
extension String {
func replace(with text: String, in range: NSRange) -> String? {
// NOTE: NSString conversion is necessary to operate in the same symbol steps
// Otherwise, you may not be able to delete an emoji, for example
let current = NSString(string: self)
guard range.location + range.length <= current.length else { return nil }
return current.replacingCharacters(in: range, with: text)
}
}
And use it like this:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let newText = textView.text.replace(with: text, in: range) else { return false }
return newText.count < maxNumberOfCharacters
// NOTE: You may wanna trim the new text instead,
// so the user will able to shove his long text at least partially
}
Otherwise, you will constantly be getting crashed in your app.
If you have multiple textField that have various length checks on one page I've found an easy and short solution.
class MultipleTextField: UIViewController {
let MAX_LENGTH_TEXTFIELD_A = 10
let MAX_LENGTH_TEXTFIELD_B = 11
lazy var textFieldA: UITextField = {
let textField = UITextField()
textField.tag = MAX_LENGTH_TEXTFIELD_A
textField.delegate = self
return textField
}()
lazy var textFieldB: UITextField = {
let textField = UITextField()
textField.tag = MAX_LENGTH_TEXTFIELD_B
textField.delegate = self
return textField
}()
}
extension MultipleTextField: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return (range.location < textField.tag) && (string.count < textField.tag)
}
}
lazy var textField: UITextField = {
let textField = UITextField()
textField.addTarget(self, #selector(handleOnEditing), for .editingChanged)
return textField
}()
//Set Delegate in ViewDidLoad
textField.delegate = self
#objc func handleOnEditing() {
let text = textField.text ?? ""
let limit = 10
textField.text = String(text.prefix(limit))
}

Resources