I have a viewController where I can enter some text into a textField and tap a done button to save it. I only want the done button to be visible if there is text in the textField. In order to do this, I used the delegate method for the UITexfield which fires when it is about to be edited as shown below. As it passes in an NSRange, I can't put that into stringByReplacingCharactersInRange as swift only allows a Range. Therefor I bridged it which allowed me to use the NSRange given. If you know a way to cast an NSRange as a Range, or even better, if you know a more concise and neater way to check if the text field is empty, please let me know. Thanks a lot.
func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {
let newString = textField.text.bridgeToObjectiveC().stringByReplacingCharactersInRange(range, withString: string)
if (newString == "" ) {
self.doneButton.enabled = false
} else {
self.doneButton.enabled = true
}
return true
}
Here is a func that will take an NSRange and replace a portion of a String:
func replaceRange(nsRange:NSRange, #ofString:String, #withString:String) ->String {
let start = nsRange.location
let length = nsRange.length
let endLocation = start + length
let ofStringLength = countElements(ofString)
if start < 0 || endLocation < start || endLocation > ofStringLength {
return ofString
}
var startIndex = advance(ofString.startIndex, start)
var endIndex = advance(startIndex, length)
var range = Range(start:startIndex, end:endIndex)
var final = ofString.stringByReplacingCharactersInRange(range, withString:withString)
return final
}
var original = "๐ช๐ธ๐This is a test"
var replacement = "!"
var nsRange:NSRange = NSMakeRange(1, 2)
var newString = replaceRange(nsRange, ofString:original, withString:replacement)
println("newString:\(newString)")
Output:
newString:๐ช๐ธ!his is a test
Instead of using bridgeToObjectiveC() simply cast your string to an NSString:
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
Here's what I like to use:
if ([textField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length > 0)
{
// do something with the text that is there ...
}
Related
I have a problem, I have two Textfields and want a max length of 20 Characters for both.
I use the following code but it only works for my first Textfield. What did I wrong? I hope someone can help me.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField === nameTextField {
let currentText = nameTextField.text
guard let stringRange = Range(range, in: currentText!) else {
return false
}
let updateText = currentText?.replacingCharacters(in: stringRange, with: string)
return updateText?.count ?? 0 < 20
} else if textField === numberTextField {
let currentText = numberTextField.text
guard let stringRange = Range(range, in: currentText!) else {
return false
}
let updateText = currentText?.replacingCharacters(in: stringRange, with: string)
return updateText?.count ?? 0 < 20
}
return true
}
You probably forgot to set the delegate for the second one.
Also no need for this complex logic! Just keep the first 20 like:
textView.text = String(textView.text.prefix(20))
I used a variable to store the text being entered and modifying it by appending the remaining suffix of "mm/dd/yyyy". I am getting the functionality, but if I try to update the cursor to the right position, its creating a problem.
I used the textfield.selectedTextRange to move the cursor from EOF to position I need. But, it is replacing the text entered with last "y" from "mm/dd/yyyy". So If I enter "12" the text changes from mm/dd/yyyy| to "yy|/mm/yyyy" instead of "12|/dd/yyy"
Am I doing it the wrong way?
let dobPlaceholderText = "mm/dd/yyyy"
var enteredDOBText = ""
#objc func textFieldDidChange(_ textField: UITextField){
enteredDOBText.append(textField.text.last ?? Character(""))
let modifiedText = enteredDOBText + dobPlaceholderText.suffix(dobPlaceholderText.count - enteredDOBText.count)
textField.text = modifiedText
setCursorPosition(input: dob.textField, position: enteredDOBText.count)
}
private func setCursorPosition(input: UITextField, position: Int){
let position = input.position(from: input.beginningOfDocument, offset: position)!
input.selectedTextRange = input.textRange(from: position, to: position)
}
The suggestion of matt and aheze in the comments to use textField(_:shouldChangeCharactersIn:replacementString:) is very promising in such use cases.
The procedure could look something like this:
determine the resulting text as it would look like after the user input and filter out all non-numeric characters
apply the pattern as defined in your dobPlaceholderText
set the result in the UIText field
determine and set new cursor position
In source code it could look like this:
let dobPlaceholderText = "mm/dd/yyyy"
let delims = "/"
let validInput = "0123456789"
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
let filteredText = filtered(textField.text, range: range, replacement: string)
let newText = applyPattern(filteredText, pattern: Array(dobPlaceholderText))
textField.text = newText
let newPosition = newCursorPosition(range: range, replacement: string, newText: newText)
setCursorPosition(input: textField, position: newPosition)
return false
}
The actual implementation of the three methods filtered, applyPattern and newCursorPosition depends on the exact detail behavior you want to achieve. This is just a sample implementation, use something that reflects your requirements.
private func filtered(_ text: String?,range:NSRange, replacement: String) -> Array<Character> {
let textFieldText: NSString = (text ?? "") as NSString
let textAfterUpdate = textFieldText.replacingCharacters(in: range, with: replacement)
var filtered = Array(textAfterUpdate.filter(validInput.contains))
if filtered.count >= dobPlaceholderText.count {
filtered = Array(filtered[0..<dobPlaceholderText.count])
}
return filtered
}
private func applyPattern(_ filtered: Array<Character>, pattern: Array<Character>) -> String {
var result = pattern
var iter = filtered.makeIterator()
for i in 0..<pattern.count {
if delims.contains(pattern[i]) {
result[i] = pattern[i]
} else if let ch = iter.next() {
result[i] = ch
} else {
result[i] = pattern[i]
}
}
return String(result)
}
private func newCursorPosition(range: NSRange, replacement: String, newText: String) -> Int {
var newPos = 0
if replacement.isEmpty {
newPos = range.location
}
else {
newPos = min(range.location + range.length + 1, dobPlaceholderText.count)
if newPos < dobPlaceholderText.count && delims.contains(Array(newText)[newPos]) {
newPos += 1
}
}
return newPos
}
Demo
I have an iOS Xcode 7.3 Swift2 project I'm working on. It has different UITextFields that are limited to 3 digits, specifically only numbers. They are assigned to the UITextFieldDelegate and it's working well.
Here is where I limit them:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return true }
let newLength = text.characters.count + string.characters.count - range.length
let limitLength = 3
if newLength > limitLength {
return false
}
let numberOnly = NSCharacterSet.init(charactersInString: "0123456789")
let stringFromTextField = NSCharacterSet.init(charactersInString: string)
let strValid = numberOnly.isSupersetOfSet(stringFromTextField)
return strValid
}
However, some of the UITextFields need to be limited to numbers still AND also limited to a single digit, how can I institute this in the section above, only for those specific UITextFields?
The names of the UITextFields that need to be single digits are:
widthInches
lengthInches
I tried placing this after the first guard section with no luck:
guard let text2 = widthInches.text else { return true }
let newLength2 = text2.characters.count + string.characters.count - range.length
let limitLength2 = 3
if newLength2 > limitLength2 {
return false
}
You can also try this code for limit textfield
actually i am using here textfield tag. Because custom textfield.
If you using custom textfield like TextfieldEffect in this condition tag will help you for limit of Textfield.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool{
if textField.tag == txtCountryCode.tag{
let maxLength = 4
let currentString: NSString = textField.text!
let newString: NSString =
currentString.stringByReplacingCharactersInRange(range, withString: string)
return newString.length <= maxLength
}
if textField.tag == txtMobileNumber.tag{
let maxLength = 10
let currentString: NSString = textField.text!
let newString: NSString =
currentString.stringByReplacingCharactersInRange(range, withString: string)
return newString.length <= maxLength
}
return true
}
I hope this will help you.
The function shouldChangeCharactersInRange passes in the particular textField as one of its parameters. You can look at that and see if it points to the same instance as the ones you want to shorten, like this:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return true }
var limitLength = 3
if textField == widthInches || textField == lengthInches {
limitLength = 1
}
let newLength = text.characters.count + string.characters.count - range.length
if newLength > limitLength {
return false
}
let numberOnly = NSCharacterSet.init(charactersInString: "0123456789")
let stringFromTextField = NSCharacterSet.init(charactersInString: string)
let strValid = numberOnly.isSupersetOfSet(stringFromTextField)
return strValid
}
Assuming all other requirements are the same (numbers only) this will do the trick.
There are other ways, for example - you could subclass UITextField and add a limitLength field, then use that field in the delegate, but that's probably overkill for just 2 exceptions.
Hello in your func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool the textField param is the textField that has trigger this event so you can check with yours textfields objects and if are equal to one of them then make a different behavior
I hope this helps you,
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
return (textField.text?.utf16.count ?? 0) + string.utf16.count - range.length <= TEXT_FIELD_LIMIT
}
This counts the number of characters based on UTF-16 representation, as range.length is given in UTF-16 base. If you need to count the number of characters in other ways, the expression may get longer. If you want only numbers to be input use textField.keyboardType = UIKeyboardTypeDecimalPad . If you want specific textFields then add tags and compare them and if they are equal you can implement your specific code for that.
Check this link for detailed answer :
http://www.globalnerdy.com/2016/05/24/a-better-way-to-program-ios-text-fields-that-have-maximum-lengths-and-accept-or-reject-specific-characters/
update for swift 3 add this class and call it TextField.swift. it will add the limit input on the storyboard.
import UIKit
private var maxLengths = [UITextField: Int]()
extension UITextField {
#IBInspectable var maxLength: Int {
get {
guard let length = maxLengths[self] else {
return Int.max
}
return length
}
set {
maxLengths[self] = newValue
// Any text field with a set max length will call the limitLength
// method any time it's edited (i.e. when the user adds, removes,
// cuts, or pastes characters to/from the text field).
addTarget(
self,
action: #selector(limitLength),
for: UIControlEvents.editingChanged
)
}
}
func limitLength(textField: UITextField) {
guard let prospectiveText = textField.text,
prospectiveText.characters.count > maxLength else {
return
}
// If the change in the text field's contents will exceed its maximum
length,
// allow only the first [maxLength] characters of the resulting text.
let selection = selectedTextRange
// text = prospectiveText.substring(with:Range<String.Index>
(prospectiveText.startIndex ..< prospectiveText.index(after: maxLength))
let s = prospectiveText
// Get range 4 places from the start, and 6 from the end.
let c = s.characters;
let r = c.index(c.startIndex, offsetBy: 0)..<c.index(c.endIndex, offsetBy: maxLength - c.count)
text = s[r]
// Access the string by the range.
selectedTextRange = selection
}
}
or download here - >TextField.swift
So I have a ViewController with three UITextFields. I do not want the user to be able to click done unless all three UITextFields are > 0.
This is the code I have. I do not get errors, but the Done button (barButtonItem) remains grayed out even when I have put something in each of the three UITextFields. What am I doing wrong?
func allTextFields(dateTextField: UITextField, numberOfLitersTextField: UITextField, costPerLiterTextField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementsString string: String) -> Bool {
let oldText: NSString = dateTextField.text!
let oldText2: NSString = numberOfLitersTextField.text!
let oldText3: NSString = costPerLiterTextField.text!
let newText: NSString = oldText.stringByReplacingCharactersInRange(range, withString: string)
let newText2: NSString = oldText2.stringByReplacingCharactersInRange(range, withString: string)
let newText3: NSString = oldText3.stringByReplacingCharactersInRange(range, withString: string)
doneBarButton.enabled = (newText.length > 0 && newText2.length > 0 && newText3.length > 0)
return true
}
I would probably do something similar to this instead:
func validateTextFields() -> Bool {
guard
let dateText = dateTextField.text,
let numberOfLitersText = numberOfLitersTextField.text,
let costPerLiterText = costPerLiterTextField.text
where !dateText.isEmpty && !numberOfLitersText.isEmpty && !costPerLiterText.isEmpty else { return false }
return true
}
and just put this inside of the UITextField delegate method that I assume you are using and just call
doneBarButton.enabled = validateTextFields()
I think that will work how you want it to.
I'm able to limit the length using the code below, but I can't seem to find a way to also limit special characters.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let newLength = count(textField.text.utf16) + count(string.utf16) - range.length
if (textField.placeholder == "USERNAME")
{
//Also limit special characters here
return newLength <= 15 // Bool
}
characters I want allowed:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.
I tried following this link but its in objective C and I'm having trouble merging it with my current 15 character limit code above
Alternative to regex, you can first get all the allowed characters into a set:
var charactesAllowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_."
var charactersSet = [Character](charactesAllowed)
then try to see if the most recently typed charactes is in this array
var newCharacter = //whatever the character is, ex: "A"
if(contains(charactersSet, newCharacter))
{
println("Allowed")
// Add it into the label text
}
let ACCEPTABLE_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
if textField == textFieldNumber{
let characterSet = CharacterSet.init(charactersIn: ACCEPTABLE_CHARACTERS).inverted
maxLength = 11
let currentString: NSString = textField.text! as NSString
let newString: NSString =
currentString.replacingCharacters(in: range, with: string) as NSString
let filter = string.components(separatedBy: characterSet).joined(separator:"")
return ((string == filter) && newString.length <= maxLength!)
}