12

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.

Community
  • 1
  • 1
Gabriel Garrett
  • 1,867
  • 5
  • 20
  • 45

19 Answers19

13

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
        }
    }
}
Steve Rosenberg
  • 18,504
  • 7
  • 42
  • 50
  • 1
    The number of digits after the decimal does not matter, but I would like to limit the number of decimals to 1, to ensure that the number being input is always a valid double. – Gabriel Garrett Oct 25 '14 at 21:24
  • Thank you! I just have one more question: How do I call the function? I tried putting the function in my viewDidLoad but on the iPad simulator I can still enter other symbols and such, so I think I am not calling it properly. – Gabriel Garrett Oct 25 '14 at 21:34
  • 1
    no - outside of viewDidLoad. It calls itself! Use the entire class as I gave it to you. – Steve Rosenberg Oct 25 '14 at 21:35
  • Ah! Thank you! The only thing that doesn't work now is the backspace. – Gabriel Garrett Oct 25 '14 at 21:41
  • 1
    sure we can add a delete key or backspace case - give me a minute or two – Steve Rosenberg Oct 25 '14 at 21:42
  • Ok awesome! Thank you, this helps a lot. You can also point me to any resources that could help me learn how to add a backspace if you don't have the time. – Gabriel Garrett Oct 25 '14 at 21:51
  • ok sure -was answering a different question for a moment. Will get you a resource soon. – Steve Rosenberg Oct 25 '14 at 21:51
  • I was ging to detect the length of the string before and after the key entry to detect a backspace. If you have issues, post a question on this and I bet you get a lot of responses, – Steve Rosenberg Oct 25 '14 at 21:55
  • 1
    updated the default case. This should now allow backspaces/delete keys. – Steve Rosenberg Oct 25 '14 at 22:02
  • Thank you so much, you are a lifesaver. – Gabriel Garrett Oct 25 '14 at 22:05
  • It was fun, and a great question! – Steve Rosenberg Oct 25 '14 at 22:05
  • This is not a good solution since it only works in a very limited set of locales. Not all users use the period as the decimal separator and not all users use the characters 0-9 for numbers. This code also will not work if the user pastes text into the text field. – rmaddy Apr 21 '16 at 14:53
  • @rmaddy good point. Could you direct me to a solution deemed suitable handling the points you mentioned? – Pavan Jun 08 '16 at 07:33
  • Hello @Steve Rosenberg, same problem I'm facing, I have restricted user to enter only number using decimal pad keyboard. Now if user enters '22.45' is ok but when he enters '22.45.2' at this point it crashes my app. So I want to restrict user to only enter Dot i.e '.' only one time using Swift. Please help me. –  Jan 30 '17 at 10:42
  • @SteveRosenberg i am trying to implement the same in xamarin.ios..so can u please see this question and help me..[UITextField KeyPress Handler in Xamarin.IOS](https://stackoverflow.com/questions/46782625/uitextfield-keypress-handler-in-xamarin-ios) – sunil y Oct 17 '17 at 11:38
  • We should not hardcode the separator as it may differ in different locales. Also a user may move the cursor and paste text. Check my answer https://stackoverflow.com/a/50947173/1433612 – Au Ris Jun 20 '18 at 11:45
10

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
   }
}
miss Gbot
  • 329
  • 3
  • 9
7

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
    }

}
Lyndsey Scott
  • 35,633
  • 9
  • 90
  • 123
7

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
        }
    }
}
wye
  • 171
  • 3
  • 7
5

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.

Miroslav Hrivik
  • 702
  • 12
  • 12
3

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
3

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 .

Abhishek Jain
  • 4,155
  • 2
  • 27
  • 29
2

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)
}
Michaël Azevedo
  • 3,764
  • 7
  • 27
  • 42
Surendra
  • 21
  • 2
  • 1
    Generally, answers are much more helpful if they include an explanation of what the code is intended to do, and why that solves the problem without introducing others. – Dan Cornilescu Apr 25 '16 at 12:52
2

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
        }
    }
Naishta
  • 9,905
  • 4
  • 60
  • 49
2

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
  }
jerem_y
  • 43
  • 5
2

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()
    }
}
RafaelPlantard
  • 109
  • 1
  • 7
1

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
    }
}
Daniel T.
  • 24,573
  • 4
  • 44
  • 59
1

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...

Greg
  • 495
  • 4
  • 12
1

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
}
pravir
  • 280
  • 2
  • 12
0
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
}
CodeSteger
  • 77
  • 3
0

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)
}
Au Ris
  • 3,967
  • 2
  • 22
  • 47
0
  • 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
}
Jakub Truhlář
  • 15,319
  • 7
  • 65
  • 73
0

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
}
Rhusfer
  • 531
  • 1
  • 9
  • 14
-1

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
    }
James Zaghini
  • 3,701
  • 3
  • 42
  • 57