2

I am currently checking my UITextField, which is set to show Numeric Keypad in shouldChangeCharactersIn to limit the input to only one decimal separator and only 2 decimal points like this (thanks to this question):

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

    let decimalSeparator = String(Locale.current.decimalSeparator ?? ".")

    if (textField.text?.contains(decimalSeparator))! {

        let limitDecimalPlace = 2
        let decimalPlace = textField.text?.components(separatedBy: decimalSeparator).last

        if (decimalPlace?.count)! < limitDecimalPlace {

            return true

        } else {

           return false

        }

    }

}

This works great. However, it is now possible to insert whatever value the user wants, which I want to limit to a value lower than 999. I used to check the length to allow only 3 characters, but now I want to allow following values (for example):

143
542.25
283.02
19.22
847.25

But I don't want to allow:

2222
3841.11
999.99

How could I do that?

Rob
  • 415,655
  • 72
  • 787
  • 1,044
PennyWise
  • 595
  • 2
  • 12
  • 37

1 Answers1

3

You probably need two checks:

  1. Make sure it in the form of xxx.xx. This sort of pattern matching is often achieved by using regular expression search.

    The trick here is to make sure you support all permutations with and without decimal place, where the fractional digits is two or fewer digits and the integer digits is three or fewer digits.

  2. Try converting it to a number and check that the value is less than 999.

Thus:

let formatter = NumberFormatter()

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

    if candidate == "" { return true }

    let isWellFormatted = candidate.range(of: "^[0-9]{1,3}([\(separator)][0-9]{0,2})?$", options: .regularExpression) != nil

    if isWellFormatted,
        let value = formatter.number(from: candidate)?.doubleValue,
        value >= 0,
        value < 999 {
            return true
    }

    return false
}

Note:

  • I’m assuming you want users to be able to honor their device’s localization settings (e.g. let a German user enter 123,45 because they use , as the decimal separator).

  • The regular expression, "^[0-9]{1,3}([\(separator)][0-9]{0,2})?$” probably looks a little hairy if you’re not used to regex.

    • The ^ matches the start of the string;
    • The [0-9] obviously matches any digit;
    • The {1,3} matches between one and three integer digits;
    • The (...)? says “optionally, look for the following”;
    • Again, [0-9]{0,2} means “between zero and two fractional digits; and
    • The $ matches the end of the string.
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • That's insane. Works like a charm. I fixed one issue where the last character could be the separator, which I didn't want. So I check if this is the case in `textFieldDidEndEditing` and drop it. Now there is just one more issue - I am able to enter "000", which I don't want, but I want to allow 0, 0.0 and 0.00. Any idea if that is possible too? – PennyWise Jan 26 '19 at 19:13
  • Re “last character could be separator”, I think you have to allow that because if you type “1”, “2”, “.”, “3” for “12.3”, you don’t want it to not permit the typing of the “.” because it happens to be the last character. Generally, the logic about whether you accept the final number and the logic about whether you allow it to change characters, are two different things. – Rob Jan 26 '19 at 19:32
  • Re 00 and 000, you can add “negative look-ahead assertion”, e.g., `"^(?!0[0-9])[0-9]{0,3}([\(separator)][0-9]{0,2})?$"`. Or you might just trim leading zeros for them, which might be more user friendly. – Rob Jan 26 '19 at 19:33
  • 1
    Perfect. I have implemented the regex search you provided as it does exactly what I want it to do. Trimming is indeed a possibility, but I like the inability to add another zero if the first character is a zero. Considering the separator, I don't delete it in the change check - I do this in the didEndEditing so the final value in the UITextField becomes "12" instead of "12.", but it would remain 12.3 because then the last character in the didEndEditing check is a 3, not the separator. I will test a bit more tonight, but so far it seems like all is fine now. Thank you a thousand times! – PennyWise Jan 26 '19 at 19:40