Related
I have found a lot of guides on how to do this in objective-c, but I would like to see a more Swift-oriented way of doing this.
I have a UITextField that a user enters a currency price into. The textfield calls a decimal pad keyboard. However, on the iPad, the keyboard that comes up has a whole range of non-decimal symbols.
Basically, for every single key press, I would like to make it impossible for a non-number or anything beyond a single decimal to be typed into the field. If a decimal is typed, I would like to make it impossible to enter a second decimal. If the decimal is deleted, I'd like to make sure the user can enter a decimal again.
Any ideas on how to properly do this in swift?
I also see solutions like the ones posted here:
Limit UITextField to one decimal point Swift
But I have no idea where to place the functions or how I should call them. Whenever I try to put in NSRange in the parameters, I receive an error that I am not creating a range properly.
Here is a simple example:
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
}
//Textfield delegates
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { // return NO to not change text
switch string {
case "0","1","2","3","4","5","6","7","8","9":
return true
case ".":
let array = Array(textField.text)
var decimalCount = 0
for character in array {
if character == "." {
decimalCount++
}
}
if decimalCount == 1 {
return false
} else {
return true
}
default:
let array = Array(string)
if array.count == 0 {
return true
}
return false
}
}
}
All of answers use '.' as valid separator for decimals, but in different localisation it's may be wrong.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard !string.isEmpty else {
return true
}
let currentText = textField.text ?? ""
let replacementText = (currentText as NSString).replacingCharacters(in: range, with: string)
return replacementText.isDecimal()
}
extension String{
func isDecimal()->Bool{
let formatter = NumberFormatter()
formatter.allowsFloats = true
formatter.locale = Locale.current
return formatter.number(from: self) != nil
}
}
This takes multiple decimals into account by using an NSScanner to test whether the new string would be numeric:
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {
// Get the attempted new string by replacing the new characters in the
// appropriate range
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
if newString.length > 0 {
// Find out whether the new string is numeric by using an NSScanner.
// The scanDecimal method is invoked with NULL as value to simply scan
// past a decimal integer representation.
let scanner: NSScanner = NSScanner(string:newString)
let isNumeric = scanner.scanDecimal(nil) && scanner.atEnd
return isNumeric
} else {
// To allow for an empty text field
return true
}
}
Swift 2 version of #Steve Rosenberg's solution
If you don't need to limit input to max 2 fractional digits (i.e, "12.34" OK, "12.345" not OK), then remove the 4 lines at the beginning.
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
}
//Textfield delegates
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { // return false to not change text
// max 2 fractional digits allowed
let newText = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
let regex = try! NSRegularExpression(pattern: "\\..{3,}", options: [])
let matches = regex.matchesInString(newText, options:[], range:NSMakeRange(0, newText.characters.count))
guard matches.count == 0 else { return false }
switch string {
case "0","1","2","3","4","5","6","7","8","9":
return true
case ".":
let array = textField.text?.characters.map { String($0) }
var decimalCount = 0
for character in array! {
if character == "." {
decimalCount++
}
}
if decimalCount == 1 {
return false
} else {
return true
}
default:
let array = string.characters.map { String($0) }
if array.count == 0 {
return true
}
return false
}
}
}
Swift 3 Implement this UITextFieldDelegate method to prevent user from typing an invalid number:
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 let regex = try? NSRegularExpression(pattern: "^[0-9]*((\\.|,)[0-9]{0,2})?$", options: .caseInsensitive) {
return regex.numberOfMatches(in: newText, options: .reportProgress, range: NSRange(location: 0, length: (newText as NSString).length)) > 0
}
return false
}
It is working with both comma or dot as decimal separator and allows 2 fraction digits.
Swift 4.2
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let numberCharSet = CharacterSet(charactersIn: ".").union(CharacterSet.decimalDigits)
let characterSet = CharacterSet(charactersIn: string)
return numberCharSet.isSuperset(of: characterSet)
}
This allows digits from 0 to 9 and decimal point .
This is inspired by wye's answer, but is a bit more compact and has worked for me where I wanted a numeric/decimal field. You can adapt to just accept integers by modifying the regex (take out .?\\d{0,2} leaving you with ^\\d*$). Likewise, if you don't want to restrict the number of digits after the decimal place, you can remove that restriction (just change it to ^\\d*\\.?\\d*)
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let newString = (_timeQuantityField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
let decimalRegex = try! NSRegularExpression(pattern: "^\\d*\\.?\\d{0,2}$", options: [])
let matches = decimalRegex.matchesInString(newString, options: [], range: NSMakeRange(0, newString.characters.count))
if matches.count == 1
{
return true
}
return false
}
This allows the numeric string to be constructed without any rejection of input along the way so, for example, the following are all valid inputs and (newString as NSString).floatValue gives a valid result):
(i.e. the empty string) yields 0.0
. yields 0.0
1. yields 1.0
.1 yields 0.1
Here is the simplest method:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if (textField.text?.componentsSeparatedByString(".").count > 1 && string == ".")
{
return false
}
return string == "" || (string == "." || Float(string) != nil)
}
Tested and works in Swift 3 and Swift 4, you can also do the checks as below
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let existingTextHasDecimalSeparator = textField.text?.rangeOfString(".")
let replacementTextHasDecimalSeparator = string.rangeOfString(".")
if existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil {
return false
}
else {
return true
}
}
Improving Naishta's response in Swift 4, here is a snippet that allows you to restrict the textfield length to 10 characters (extra bonus - not requested by post creator) and a single decimal point:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return true }
// Max 10 characters.
let newLength = text.count + string.count - range.length
if newLength > 10 { return false }
// Max one decimal point.
let existingTextHasDecimalSeparator = text.range(of: ".")
let replacementTextHasDecimalSeparator = string.range(of: ".")
if existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil { return false }
return true
}
Here's a Swift 4 solution:
import struct Foundation.CharacterSet
extension String {
var onlyNumbers: String {
let charset = CharacterSet.punctuationCharacters.union(CharacterSet.decimalDigits).inverted
return components(separatedBy: charset).joined()
}
}
Do it the same way. The code below doesn't guard against multiple . but otherwise does what you want. Extend it as you will.
class Foo: NSObject, UITextFieldDelegate {
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
var result = true
if countElements(string) > 0 {
let numericInput = NSCharacterSet(charactersInString: "0123456789.-").invertedSet
if let badRange = string.rangeOfCharacterFromSet(numericInput) {
let substring = string.substringToIndex(badRange.startIndex)
let oldString: NSString = textField.text // necessary so we can use the NSRange object passed in.
textField.text = oldString.stringByReplacingCharactersInRange(range, withString: substring)
result = false
}
}
return result
}
}
Here is what I use. If this returns false, the caller will remove the last (offending) character with textField.deleteBackward().
func isValidNumber(text: String) -> Bool {
let validChars: Set<Character> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."]
return (Set(text).isSubset(of: validChars) && ((text.components(separatedBy: ".").count - 1) <= 1))
}
Or you could do it all within the function:
func isValidNumber2(textField: UITextField) -> Bool {
let validChars: Set<Character> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."]
let validNum = Set(textField.text!).isSubset(of: validChars) && ((textField.text!.components(separatedBy: ".").count - 1) <= 1)
if !validNum {
textField.deleteBackward()
}
return (validNum)
}
Both are short, clear, simple, and efficient. (Seems the second one is cleaner... Opinions?) But they don't limit input to a single decimal point...
Swift 4
Used #SteveRosenberg's answer and wrote this according to my requirements
max number of Integers Numbers is 4 i.e., 9999, and max decimal digits limit is 2. So, max number can be 9999.99
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// 100 is the tag value of our textfield
/*or you may use "if textfield == myTextField{" if you have an IBOutlet to that textfield */
if textField.tag == 100 {
//max length limit of text is 8
if textField.text!.count > 8 && string != "" {
return false
}
let maxLength = 8
let currentString: NSString = textField.text! as NSString
// Use following code If you are inputting price to that text field and want $ to get inserted automatically at start when user starts typing in that textfield or you may put some other character at start instead of $. Otherwise comment the following 3 lines of if condition code
if currentString.length == 0 {
priceTextField.text = "$"
}
//new string after inserting the new entered characters
let newString: NSString =
currentString.replacingCharacters(in: range, with: string) as NSString
if newString.length > maxLength{
return false
}
if (textField.text!.range(of: ".") != nil) {
let numStr = newString.components(separatedBy: ".")
if numStr.count>1{
let decStr = numStr[1]
if decStr.length > 2{
return false
}
}
}
var priceStr: String = newString as String
if (textField.text!.range(of: "$") != nil) {
priceStr = priceStr.replacingOccurrences(of: "$", with: "")
}
let price: Double = Double(priceStr) ?? 0
if price > 9999.99{
return false
}
switch string {
case "0","1","2","3","4","5","6","7","8","9":
return true
case ".":
let array = Array(textField.text!)
var decimalCount = 0
for character in array {
if character == "." {
decimalCount = decimalCount + 1
}
}
if decimalCount == 1 {
return false
} else {
return true
}
default:
let array = Array(string)
if array.count == 0 {
return true
}
return false
}
}
return true
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if (range.location == 0 && string == ".") {
return false
}
else if string == "."{
if textField.text?.componentsSeparatedByString(".").count > 1{
return false
}
}
let aSet = NSCharacterSet(charactersInString:"0123456789.").invertedSet
let compSepByCharInSet = string.componentsSeparatedByCharactersInSet(aSet)
let numberFiltered = compSepByCharInSet.joinWithSeparator("")
return string == numberFiltered
}
We can do better without hardcoding the allowed characters and the separator. Especially the separator, as it may be different in different locales. Also we need to be aware that a user may move the cursor and paste text. Here is a validation function which takes that into account:
static func validateDecimalNumberText(for textField: UITextField, replacementStringRange: NSRange, string: String) -> Bool {
// Back key
if string.isEmpty {
return true
}
// Allowed charachters include decimal digits and the separator determined by number foramtter's (current) locale
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 2
let allowedCharacters = CharacterSet.decimalDigits.union(CharacterSet(charactersIn: numberFormatter.decimalSeparator))
let characterSet = CharacterSet(charactersIn: string)
// False if string contains not allowed characters
if !allowedCharacters.isSuperset(of: characterSet) {
return false
}
// Check for decimal separator
if let input = textField.text {
if let range = input.range(of: numberFormatter.decimalSeparator) {
let endIndex = input.index(input.startIndex, offsetBy: input.distance(from: input.startIndex, to: range.upperBound))
let decimals = input.substring(from: endIndex)
// If the replacement string contains a decimal seperator and there is already one, return false
if input.contains(numberFormatter.decimalSeparator) && string == numberFormatter.decimalSeparator {
return false
}
// If a replacement string is before the separator then true
if replacementStringRange.location < endIndex.encodedOffset {
return true
} else {
// If the string will exceed the max number of fraction digits, then return false, else true
return string.count + decimals.count <= numberFormatter.maximumFractionDigits
}
}
}
return true
}
And the textfield delegate method:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return Utils.validateDecimalNumberText(for: textField, replacementStringRange: range, string: string)
}
Only numbers.
2 decimal places.
No spaces.
The decimal mark is either a dot or a comma.
If you need to specify the decimal mark, change the [.,].
let regex = try! NSRegularExpression(pattern: "^[0-9]*([.,][0-9]{0,2})?$", options: .caseInsensitive)
if let newText = (textFieldView.textField.text as NSString?)?.replacingCharacters(in: range, with: string) {
return regex.firstMatch(in: newText, options: [], range: NSRange(location: 0, length: newText.count)) != nil
} else {
return false
}
Right now I am using this solution without regex. Hope it helps :D
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }
if textField == txtFieldWeight || textField == txtFieldHeight {
let newText = currentText.replacingOccurrences(of: ",", with: ".")
let isDecimal = Float(newText) != nil
return isDecimal
}
return true
}
SWIFT 3.2 and 4.0
Chis will limit user to two digits after decimal and also will limit them to add one decimal point.
Make sure you set the keyboard type to decimal.
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// if keyboard type is decimal then apply just one dot
if(textField.keyboardType == .decimalPad)
{
// geting counts of dot
let countdots = (textField.text?.components(separatedBy:".").count)! - 1
// if there is more then one dot then
if(countdots > 0)
{
// creating array by dot
var digitArray = textField.text?.components(separatedBy:".")
let decimalDigits = digitArray![1]
// limiting only 2 digits after decimal point
if(decimalDigits.count > 1 )
{
return false;
}
}
// limiting to only 1 decimal point
if countdots > 0 && string == "."
{
return false
}
}
return true
}
I want the user to enter a Social Security Number in the format ••• •• ••••.
The user types the first 3 numbers, then I append a space manually. Then they enter 2 more numbers and I manually append a space. Of course, even the spaces are being displayed as •. Is there a native way to change this behavior? I am currently using a funky manual implementation of this.
What if instead of spaces, you use three different text entries? Then when each user enters the first three characters, you jump to the second text entry? When he types two more, you jump to the third text entry.
Here is an example:
EDIT: Now supports backspace (Thanks to #triple-s).
extension ViewController: UITextFieldDelegate {
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
switch textField {
// Jump forwards
case self.textField1 where string.characters.count == 3 :
self.textField2.becomeFirstResponder()
case self.textField2 where string.characters.count == 2 :
self.textField3.becomeFirstResponder()
// Jump backwards
case self.textField3 where string.characters.count == 0 :
self.textField2.becomeFirstResponder()
case self.textField2 where string.characters.count == 0 :
self.textField1.becomeFirstResponder()
default :
break
}
return true
}
}
This can be achieve in one single textField as asked. I only tapped "1", in the gif.
You select your keypad type to be number (0-9), which can ensure everything that will be input there is number only.
Then you can adopt the textField delegate and implement the delegate method
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
print(string)
let char = string.cStringUsingEncoding(NSUTF8StringEncoding)!
let isBackSpace = { return strcmp(char, "\\b") == -92}
if (textField.text?.characters.count == 3 && !isBackSpace()) || (textField.text?.characters.count == 6 && !isBackSpace()){
textField.text = textField.text! + " "
}
if (textField.text?.characters.count) == 11 && !isBackSpace(){
ssnString = textField.text!
self.view.endEditing(true)
}
return true
}
This includes the logic of adding space after third digit if you are not backspacing and same for the after 6th digit.
Also, after user input 11 digit, it will not allow user to input more number as the format of SSN, after 11 digit is input, the SSN is saved in ssnString, which will be used for you special masking.
Because you don't want to mask space, we can not use secureTextEntry. So in the didEndEditing, I gave an condition only if the user enter the full SSN, we will mask it, which can be modified to any scenario if you want. But i think this makes more sense.
func textFieldDidEndEditing(textField: UITextField) {
if textField.text?.characters.count == 11 {
maskSSNTextField()
}
}
In the maskSSNTextField method,
func maskSSNTextField() {
textField.text = "••• •• ••••"
}
Finally, we need to unmask it when user come back to it, if they want to change the text
func textFieldDidBeginEditing(textField: UITextField) {
if textField.text == "••• •• ••••"{
textField.text = ssnString
}
}
This fully fulfilled your requirement. Please let me know if you have other question.
I changed the didChangeInRange method to meet your new requirement, although I think my previous answer could work. Now it works as in the gif. If you want it to be still masked, you can change the code in textField did begin editing.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let char = string.cStringUsingEncoding(NSUTF8StringEncoding)!
let isBackSpace = { return strcmp(char, "\\b") == -92}
if (textField.text?.characters.count == 3 && !isBackSpace()) || (textField.text?.characters.count == 6 && !isBackSpace()){
textField.text = textField.text! + " "
ssnString = ssnString + " "
}
if isBackSpace() {
ssnString = ssnString.substringToIndex(ssnString.endIndex.predecessor())
}else {
ssnString = ssnString + string
print(ssnString)
if ssnString.characters.count >= 2 {
var starString = ""
for i in 0...ssnString.characters.count-2 {
if i==3 || i==6 {
starString = starString+" "
}else {
starString = starString+"•"
}
}
textField.text = ""
print(ssnString.characters.last)
textField.text = starString
}
}
if (textField.text?.characters.count) == 11 && !isBackSpace(){
self.view.endEditing(true)
}
return true
}
The simple solution I have been using is to convert my input string to an NSAttributedString with text spacing (.kern) attributes added at the proper locations and keeping isSecureTextEntry set to true.
Disabling isSecureTextEntry and doing it by hand in addition of being overly complex could have security implications at least if someone is using a third party keyboard.
var ssnText = "123456789"
let spacerPositions = [ 2, 4 ]
let spacingAmount: CGFloat = 5.0
let spacerRanges:[NSRange] = spacerPositions
.filter { $0 < ssnText.count - 1 }
.map { NSRange(location: $0, length: 1) }
let attributedString = NSMutableAttributedString(string: ssnText)
for range in spacerRanges {
attributedString.addAttribute(.kern, value: spacingAmount, range: range)
}
textField.attributedText = attributedString
calling that stuff in textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String).
I am having a UITextField in which i get the month number as input. I am successful in limiting the no of characters to 2 in the UITextField. But i want users to enter only the values from 1 to 12 and none other than that. This has to be done simultaneously when the user types the numbers i.e in func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool. If i use a simple if condition to check the each character and return false in else part the textfield won't allow me to use clear or retype any other character. someone help me.
Set keyboard type as Number Pad
add this
func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {
if let text = textField.text {
let newStr = (text as NSString)
.stringByReplacingCharactersInRange(range, withString: string)
if newStr.isEmpty {
return true
}
let intvalue = Int(newStr)
return (intvalue >= 0 && intvalue <= 12)
}
return true
}
You can do it simultaneously by checking the TextField value inside shouldChangeCharactersInRange.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let inputStr = textField.text?.stringByAppendingString(string)
let inputInt = Int(inputStr!)
if inputInt > 0 && inputInt < 13 {
return true
} else {
return false
}
}
=> you can Define limite of char like this:-
#define NUMBERS_ONLY #"1234567890"
#define CHARACTER_LIMIT 2
=> and based on define limit char you can use and try it below method :-
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSUInteger newLength = [textField.text length] + [string length] - range.length;
NSCharacterSet *cs = [[NSCharacterSet characterSetWithCharactersInString:NUMBERS_ONLY] invertedSet];
NSString *filtered = [[string componentsSeparatedByCharactersInSet:cs] componentsJoinedByString:#""];
return (([string isEqualToString:filtered])&&(newLength <= CHARACTER_LIMIT));
}
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {
// Create an `NSCharacterSet` set which includes everything *but* the digits
let inverseSet = NSCharacterSet(charactersInString:"0123456789").invertedSet
// At every character in this "inverseSet" contained in the string,
// split the string up into components which exclude the characters
// in this inverse set
let components = string.componentsSeparatedByCharactersInSet(inverseSet)
// Rejoin these components
let filtered = components.joinWithSeparator("") // use join("", components) if you are using Swift 1.2
// If the original string is equal to the filtered string, i.e. if no
// inverse characters were present to be eliminated, the input is valid
// and the statement returns true; else it returns false
return string == filtered
}
see this link-- Limit UITextField input to numbers in Swift
Check out this to set Limit the numbers and allow only numbers 0 to 9.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == mobileNumber {
let aSet = NSCharacterSet(charactersIn:"0123456789").inverted
let compSepByCharInSet = string.components(separatedBy: aSet)
let numberFiltered = compSepByCharInSet.joined(separator: "")
let length = (mobileNumber.text?.count)! + string.count - range.length
return string == numberFiltered && length <= LIMIT
}else if textField == userType {
return false
}
return true
}
I just want to post a more simplified answer based on the previous answers.
Tested on Swift 5.1
Considering that you already set textField.keyboardType = .numberPad, then you can do the following:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else {
return true
}
let newStr = (text as NSString).replacingCharacters(in: range, with: string)
guard let intValue = Int(newStr) else {
return true
}
return intValue <= maxNumber // maxNumber: replace with your max number
}
You dont´need to validate that intValue is greater or equal to 0 because in numberPad you can NOT write negative values.
This code works perfectly, and I can't key in anything other than integers, even when I try to paste it in.
I'd like to add one more refinement, which is to limit the length of the input. Here's my code:
func initializeTextFields()
{
APTeams.delegate = self
APTeams.keyboardType = UIKeyboardType.NumberPad
APRounds.delegate = self
APRounds.keyboardType = UIKeyboardType.NumberPad
APBreakers.delegate = self
APBreakers.keyboardType = UIKeyboardType.NumberPad
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
// Find out what the text field will be after adding the current edit
let text = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
if text == "" {
return true
}
if let _ = Int(text) {
return true
}
else {
return false
}
}
What do I have to add to it to achieve this? The maximum input length for all the TextFields should be <= 4.
BTW, all code is in Swift 2. From problems I faced when trying to implement answers to questions I've asked before, I gather that some of the methods are different.
count(textField.text) is deprecated in SWIFT 2.0
public func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if let textField = textField as? UITextField {
if (range.length + range.location > textField.text!.characters.count) {
return false;
}
let newLength = textField.text!.characters.count + string.characters.count - range.length;
switch(textField.tag) { //In case you want to handle multiple textfields
case Constants.TAG1:
return newLength <= 20;
case Constants.TAG2:
return newLength <= 30;
default:
return newLength <= 15;
}
}
return true;
}
Write the condition in textfield delegate method as:-
func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {
if (count(textField.text) > 4 && range.length == 0)
{
return false // return NO to not change text
}
else
{
}
write all your code part in else part.
The delegate methods or an NSFormatter such as NSNumberFormatter.
The formatter is the most appropriate generally as it also provides localization support.
I know its bit too late but still I want share it too, I found a way which is much easier to set a limit character for an textfield in swift development.
Here is the code:-
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
addTarget(self, action: #selector(limitLength), for: .editingChanged)
}
}
#objc func limitLength(textField: UITextField) {
guard let prospectiveText = textField.text, prospectiveText.count > maxLength else {
return
}
let selection = selectedTextRange
let maxCharIndex = prospectiveText.index(prospectiveText.startIndex, offsetBy: maxLength)
#if swift(>=4.0)
text = String(prospectiveText[..<maxCharIndex])
#else
text = prospectiveText.substring(to: maxCharIndex)
#endif
selectedTextRange = selection
}
}
and just set the limit through the panel.
Image:
Just try this to limit the length of TF
Editing changed Action Outlet of TF
#IBAction func otpTF2EditingChnaged(_ sender: UITextField) {
if (sender.text?.count == 1) {
otpTF3.becomeFirstResponder()
}
checkMaxLength(textField: sender , maxLength: 1)
}
Function That will limit the length
private func checkMaxLength(textField: UITextField!, maxLength: Int) {
if (textField.text!.count > maxLength) {
textField.deleteBackward()
}
}
I have found a lot of guides on how to do this in objective-c, but I would like to see a more Swift-oriented way of doing this.
I have a UITextField that a user enters a currency price into. The textfield calls a decimal pad keyboard. However, on the iPad, the keyboard that comes up has a whole range of non-decimal symbols.
Basically, for every single key press, I would like to make it impossible for a non-number or anything beyond a single decimal to be typed into the field. If a decimal is typed, I would like to make it impossible to enter a second decimal. If the decimal is deleted, I'd like to make sure the user can enter a decimal again.
Any ideas on how to properly do this in swift?
I also see solutions like the ones posted here:
Limit UITextField to one decimal point Swift
But I have no idea where to place the functions or how I should call them. Whenever I try to put in NSRange in the parameters, I receive an error that I am not creating a range properly.
Here is a simple example:
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
}
//Textfield delegates
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { // return NO to not change text
switch string {
case "0","1","2","3","4","5","6","7","8","9":
return true
case ".":
let array = Array(textField.text)
var decimalCount = 0
for character in array {
if character == "." {
decimalCount++
}
}
if decimalCount == 1 {
return false
} else {
return true
}
default:
let array = Array(string)
if array.count == 0 {
return true
}
return false
}
}
}
All of answers use '.' as valid separator for decimals, but in different localisation it's may be wrong.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard !string.isEmpty else {
return true
}
let currentText = textField.text ?? ""
let replacementText = (currentText as NSString).replacingCharacters(in: range, with: string)
return replacementText.isDecimal()
}
extension String{
func isDecimal()->Bool{
let formatter = NumberFormatter()
formatter.allowsFloats = true
formatter.locale = Locale.current
return formatter.number(from: self) != nil
}
}
This takes multiple decimals into account by using an NSScanner to test whether the new string would be numeric:
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {
// Get the attempted new string by replacing the new characters in the
// appropriate range
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
if newString.length > 0 {
// Find out whether the new string is numeric by using an NSScanner.
// The scanDecimal method is invoked with NULL as value to simply scan
// past a decimal integer representation.
let scanner: NSScanner = NSScanner(string:newString)
let isNumeric = scanner.scanDecimal(nil) && scanner.atEnd
return isNumeric
} else {
// To allow for an empty text field
return true
}
}
Swift 2 version of #Steve Rosenberg's solution
If you don't need to limit input to max 2 fractional digits (i.e, "12.34" OK, "12.345" not OK), then remove the 4 lines at the beginning.
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
}
//Textfield delegates
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { // return false to not change text
// max 2 fractional digits allowed
let newText = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
let regex = try! NSRegularExpression(pattern: "\\..{3,}", options: [])
let matches = regex.matchesInString(newText, options:[], range:NSMakeRange(0, newText.characters.count))
guard matches.count == 0 else { return false }
switch string {
case "0","1","2","3","4","5","6","7","8","9":
return true
case ".":
let array = textField.text?.characters.map { String($0) }
var decimalCount = 0
for character in array! {
if character == "." {
decimalCount++
}
}
if decimalCount == 1 {
return false
} else {
return true
}
default:
let array = string.characters.map { String($0) }
if array.count == 0 {
return true
}
return false
}
}
}
Swift 3 Implement this UITextFieldDelegate method to prevent user from typing an invalid number:
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 let regex = try? NSRegularExpression(pattern: "^[0-9]*((\\.|,)[0-9]{0,2})?$", options: .caseInsensitive) {
return regex.numberOfMatches(in: newText, options: .reportProgress, range: NSRange(location: 0, length: (newText as NSString).length)) > 0
}
return false
}
It is working with both comma or dot as decimal separator and allows 2 fraction digits.
Swift 4.2
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let numberCharSet = CharacterSet(charactersIn: ".").union(CharacterSet.decimalDigits)
let characterSet = CharacterSet(charactersIn: string)
return numberCharSet.isSuperset(of: characterSet)
}
This allows digits from 0 to 9 and decimal point .
This is inspired by wye's answer, but is a bit more compact and has worked for me where I wanted a numeric/decimal field. You can adapt to just accept integers by modifying the regex (take out .?\\d{0,2} leaving you with ^\\d*$). Likewise, if you don't want to restrict the number of digits after the decimal place, you can remove that restriction (just change it to ^\\d*\\.?\\d*)
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let newString = (_timeQuantityField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
let decimalRegex = try! NSRegularExpression(pattern: "^\\d*\\.?\\d{0,2}$", options: [])
let matches = decimalRegex.matchesInString(newString, options: [], range: NSMakeRange(0, newString.characters.count))
if matches.count == 1
{
return true
}
return false
}
This allows the numeric string to be constructed without any rejection of input along the way so, for example, the following are all valid inputs and (newString as NSString).floatValue gives a valid result):
(i.e. the empty string) yields 0.0
. yields 0.0
1. yields 1.0
.1 yields 0.1
Here is the simplest method:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if (textField.text?.componentsSeparatedByString(".").count > 1 && string == ".")
{
return false
}
return string == "" || (string == "." || Float(string) != nil)
}
Tested and works in Swift 3 and Swift 4, you can also do the checks as below
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let existingTextHasDecimalSeparator = textField.text?.rangeOfString(".")
let replacementTextHasDecimalSeparator = string.rangeOfString(".")
if existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil {
return false
}
else {
return true
}
}
Improving Naishta's response in Swift 4, here is a snippet that allows you to restrict the textfield length to 10 characters (extra bonus - not requested by post creator) and a single decimal point:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return true }
// Max 10 characters.
let newLength = text.count + string.count - range.length
if newLength > 10 { return false }
// Max one decimal point.
let existingTextHasDecimalSeparator = text.range(of: ".")
let replacementTextHasDecimalSeparator = string.range(of: ".")
if existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil { return false }
return true
}
Here's a Swift 4 solution:
import struct Foundation.CharacterSet
extension String {
var onlyNumbers: String {
let charset = CharacterSet.punctuationCharacters.union(CharacterSet.decimalDigits).inverted
return components(separatedBy: charset).joined()
}
}
Do it the same way. The code below doesn't guard against multiple . but otherwise does what you want. Extend it as you will.
class Foo: NSObject, UITextFieldDelegate {
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
var result = true
if countElements(string) > 0 {
let numericInput = NSCharacterSet(charactersInString: "0123456789.-").invertedSet
if let badRange = string.rangeOfCharacterFromSet(numericInput) {
let substring = string.substringToIndex(badRange.startIndex)
let oldString: NSString = textField.text // necessary so we can use the NSRange object passed in.
textField.text = oldString.stringByReplacingCharactersInRange(range, withString: substring)
result = false
}
}
return result
}
}
Here is what I use. If this returns false, the caller will remove the last (offending) character with textField.deleteBackward().
func isValidNumber(text: String) -> Bool {
let validChars: Set<Character> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."]
return (Set(text).isSubset(of: validChars) && ((text.components(separatedBy: ".").count - 1) <= 1))
}
Or you could do it all within the function:
func isValidNumber2(textField: UITextField) -> Bool {
let validChars: Set<Character> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."]
let validNum = Set(textField.text!).isSubset(of: validChars) && ((textField.text!.components(separatedBy: ".").count - 1) <= 1)
if !validNum {
textField.deleteBackward()
}
return (validNum)
}
Both are short, clear, simple, and efficient. (Seems the second one is cleaner... Opinions?) But they don't limit input to a single decimal point...
Swift 4
Used #SteveRosenberg's answer and wrote this according to my requirements
max number of Integers Numbers is 4 i.e., 9999, and max decimal digits limit is 2. So, max number can be 9999.99
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// 100 is the tag value of our textfield
/*or you may use "if textfield == myTextField{" if you have an IBOutlet to that textfield */
if textField.tag == 100 {
//max length limit of text is 8
if textField.text!.count > 8 && string != "" {
return false
}
let maxLength = 8
let currentString: NSString = textField.text! as NSString
// Use following code If you are inputting price to that text field and want $ to get inserted automatically at start when user starts typing in that textfield or you may put some other character at start instead of $. Otherwise comment the following 3 lines of if condition code
if currentString.length == 0 {
priceTextField.text = "$"
}
//new string after inserting the new entered characters
let newString: NSString =
currentString.replacingCharacters(in: range, with: string) as NSString
if newString.length > maxLength{
return false
}
if (textField.text!.range(of: ".") != nil) {
let numStr = newString.components(separatedBy: ".")
if numStr.count>1{
let decStr = numStr[1]
if decStr.length > 2{
return false
}
}
}
var priceStr: String = newString as String
if (textField.text!.range(of: "$") != nil) {
priceStr = priceStr.replacingOccurrences(of: "$", with: "")
}
let price: Double = Double(priceStr) ?? 0
if price > 9999.99{
return false
}
switch string {
case "0","1","2","3","4","5","6","7","8","9":
return true
case ".":
let array = Array(textField.text!)
var decimalCount = 0
for character in array {
if character == "." {
decimalCount = decimalCount + 1
}
}
if decimalCount == 1 {
return false
} else {
return true
}
default:
let array = Array(string)
if array.count == 0 {
return true
}
return false
}
}
return true
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if (range.location == 0 && string == ".") {
return false
}
else if string == "."{
if textField.text?.componentsSeparatedByString(".").count > 1{
return false
}
}
let aSet = NSCharacterSet(charactersInString:"0123456789.").invertedSet
let compSepByCharInSet = string.componentsSeparatedByCharactersInSet(aSet)
let numberFiltered = compSepByCharInSet.joinWithSeparator("")
return string == numberFiltered
}
We can do better without hardcoding the allowed characters and the separator. Especially the separator, as it may be different in different locales. Also we need to be aware that a user may move the cursor and paste text. Here is a validation function which takes that into account:
static func validateDecimalNumberText(for textField: UITextField, replacementStringRange: NSRange, string: String) -> Bool {
// Back key
if string.isEmpty {
return true
}
// Allowed charachters include decimal digits and the separator determined by number foramtter's (current) locale
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 2
let allowedCharacters = CharacterSet.decimalDigits.union(CharacterSet(charactersIn: numberFormatter.decimalSeparator))
let characterSet = CharacterSet(charactersIn: string)
// False if string contains not allowed characters
if !allowedCharacters.isSuperset(of: characterSet) {
return false
}
// Check for decimal separator
if let input = textField.text {
if let range = input.range(of: numberFormatter.decimalSeparator) {
let endIndex = input.index(input.startIndex, offsetBy: input.distance(from: input.startIndex, to: range.upperBound))
let decimals = input.substring(from: endIndex)
// If the replacement string contains a decimal seperator and there is already one, return false
if input.contains(numberFormatter.decimalSeparator) && string == numberFormatter.decimalSeparator {
return false
}
// If a replacement string is before the separator then true
if replacementStringRange.location < endIndex.encodedOffset {
return true
} else {
// If the string will exceed the max number of fraction digits, then return false, else true
return string.count + decimals.count <= numberFormatter.maximumFractionDigits
}
}
}
return true
}
And the textfield delegate method:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return Utils.validateDecimalNumberText(for: textField, replacementStringRange: range, string: string)
}
Only numbers.
2 decimal places.
No spaces.
The decimal mark is either a dot or a comma.
If you need to specify the decimal mark, change the [.,].
let regex = try! NSRegularExpression(pattern: "^[0-9]*([.,][0-9]{0,2})?$", options: .caseInsensitive)
if let newText = (textFieldView.textField.text as NSString?)?.replacingCharacters(in: range, with: string) {
return regex.firstMatch(in: newText, options: [], range: NSRange(location: 0, length: newText.count)) != nil
} else {
return false
}
Right now I am using this solution without regex. Hope it helps :D
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }
if textField == txtFieldWeight || textField == txtFieldHeight {
let newText = currentText.replacingOccurrences(of: ",", with: ".")
let isDecimal = Float(newText) != nil
return isDecimal
}
return true
}
SWIFT 3.2 and 4.0
Chis will limit user to two digits after decimal and also will limit them to add one decimal point.
Make sure you set the keyboard type to decimal.
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// if keyboard type is decimal then apply just one dot
if(textField.keyboardType == .decimalPad)
{
// geting counts of dot
let countdots = (textField.text?.components(separatedBy:".").count)! - 1
// if there is more then one dot then
if(countdots > 0)
{
// creating array by dot
var digitArray = textField.text?.components(separatedBy:".")
let decimalDigits = digitArray![1]
// limiting only 2 digits after decimal point
if(decimalDigits.count > 1 )
{
return false;
}
}
// limiting to only 1 decimal point
if countdots > 0 && string == "."
{
return false
}
}
return true
}