3

How can I limit a UITextField to one decimal point with Swift? The text field is for entering a price, so I cannot allow more than one decimal point. If I was using Objective-C, I would have used this code:

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSString *newString = [textField.text stringByReplacingCharactersInRange:range withString:string];

    NSArray *sep = [newString componentsSeparatedByString:@"."];
    if([sep count] >= 2)
    {
        NSString *sepStr=[NSString stringWithFormat:@"%@",[sep objectAtIndex:1]];
        return !([sepStr length]>1);
    }
    return YES;
}

But due a difference with how Swift uses ranges, I cannot convert this code to Swift. The first line gives me an error saying NSRange is not convertible to Range<String.Index>

EDIT: I ended up doing it like this before I saw the answer:

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {
    let tempRange = textField.text.rangeOfString(".", options: NSStringCompareOptions.LiteralSearch, range: nil, locale: nil)
    if tempRange?.isEmpty == false && string == "." {
        return false
    }

    return true
}

I found this on a different post. This solution works fine but I'm not sure if it is the best way to do it but it is a short and clean way.

Meet Doshi
  • 4,241
  • 10
  • 40
  • 81
Shan
  • 3,057
  • 4
  • 21
  • 33
  • I have tried implementing this function, but I am not sure how to call it for every key press. I see errors if I try to leave NSRange as the parameter for NSRange. Could I see an implementation of how you got this function to call on every keypress? – Gabriel Garrett Oct 25 '14 at 20:50

10 Answers10

6

Swift 4 & Xcode 9.2

This solution allows only one decimal point to be input, therefore allowing a valid double.

// Only allows one decimal point
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

    let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
    let arrayOfString = newString.components(separatedBy: ".")

    if arrayOfString.count > 2 {
        return false
    }
    return true
}
Devbot10
  • 1,193
  • 18
  • 33
4

Alexey got there before me, but seeing as I'd implemented a Swift version to check things out, here it is:

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

    // I'm explicitly unwrapping newString here, as I want to use reverse() on it, and that
    // apparently doesn't work with implicitly unwrapped Strings.
    if let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string) {

        var decimalFound = false
        var charactersAfterDecimal = 0

        for ch in reverse(newString) {
            if ch == "." {
                decimalFound = true
                break
            }
            charactersAfterDecimal++
        }
        if decimalFound && charactersAfterDecimal > 1 {
            return false
        }
    }
    return true;
}

Note that the first line now explicitly casts the textField.text to an NSString. This is because the String version of stringByReplacingCharactersInRange takes a Swift range, not an NSRange like the one that's passed in.

I've also made the later code a lot more Swiftian, removing NSString operations. (I'm not convinced that this is wonderfully efficient, as reverse(String) might actually reverse the whole String rather than providing a simple backward iterator, but it looks like the most Swifty way of doing it.)

Matt Gibson
  • 37,886
  • 9
  • 99
  • 128
  • Thanks, can you please check my edit on the original question? Is that a good way to do it? I can't believe I didn't think about converting it into a NSString. Those late night coding sessions make you forget the basics sometimes. – Shan Aug 10 '14 at 20:20
  • I tried this code but it allows me type in any number of ".". I actually want to limit the number of "." to 1 and also limit the number of digits after the "." to 2. I am thinking about combining your code with the one in my edited question to make it work. – Shan Aug 11 '14 at 22:44
1

Your first line in Swift will look like:

var newString = NSString(string: textField.text).stringByReplacingCharactersInRange(range, withString: string)
Alexey Globchastyy
  • 2,175
  • 1
  • 15
  • 19
  • Yup. Or `(textField.text as NSString).stringByReplacingCharacters...`. Basically much of the API is understandably still using NSString, which has NSRange ranges, which are the same concept but incompatible with Swift String ranges. Having a read of [Strings in Swift](http://oleb.net/blog/2014/07/swift-strings/) might help clear things up; it's mentioned in passing there, but the whole article is good for understanding Swift Strings in general. – Matt Gibson Aug 10 '14 at 08:42
  • Thanks, can you please check my edit on the original question? Is that a good way to do it? I can't believe I didn't think about converting it into a NSString. Those late night coding sessions make you forget the basics sometimes. – Shan Aug 10 '14 at 20:21
  • @Shan If I use the updated code in your question, I seem to be able to type plenty of numbers after the decimal point. Aha! Do you only mean that you want to limit the number of "."s in the text field? Your original code limits the number of decimal *places*—it won't let you type "0.00", for example, only "0.0"—which is a very different requirement. So, yes, if you only want to limit the number of decimal point characters, your new code should be fine, but it's not the same as what your original is doing. – Matt Gibson Aug 11 '14 at 07:30
  • @MattGibson I actually didn't notice that. Thanks for catching that. I will try using the method you suggested. – Shan Aug 11 '14 at 20:29
1

I'm not sure about the performance but this is the simplest solution so far.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if let text = textField.text {
        // Allow only one dot character in the text field
        if text.contains(".") && string == "." {
            return false
        }
    }
}
Jason Ou
  • 21
  • 2
0

I suggest using NSNumberFormatter's decimalSeparator property to split your String. Here's how to limit your input to one decimal place only using the NSNumberFormatter:

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

    if string.isEmpty { // back key
        return true
    }

    if let input = textField.text {
        let numberFormatter = NSNumberFormatter()
        let range = input.rangeOfString(numberFormatter.decimalSeparator)
        if let r = range {
            let endIndex = input.startIndex.advancedBy(input.startIndex.distanceTo(r.endIndex))
            let decimals = input.substringFromIndex(endIndex)
            return decimals.characters.count < 1
        }
    }

    return true
}
Kukiwon
  • 1,222
  • 12
  • 20
0
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)
    }
Dimple
  • 788
  • 1
  • 11
  • 27
Surendra
  • 21
  • 2
0

Try this for one decimal entry:-

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


    if(string == "." ){
        let countdots = textField.text!.componentsSeparatedByString(".").count - 1

        if countdots > 0 && string == "."
        {
            return false
        }
    }
    return true

}
Abhijeet Mallick
  • 1,740
  • 2
  • 16
  • 21
0

I hope it might help you:

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

 let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string) 

 let stringArr: [String] =   newString.components(separatedBy: ".")

if stringArr.count < 2 || (stringArr.count == 2 && stringArr[1].count == 1){ 
     return true

  } 
 return false
  }
Y_Y
  • 331
  • 1
  • 6
0
// decimal points limit on numbers text fields
    @objc static func canEnterDigits(upTo decimalPoint: Int, text: String) -> Bool {
        
        if self.isNumber(text) || text == "" {
            
            let stringArr: [String] = text.components(separatedBy: ".")
            if stringArr.count >= 2 {
                let sepString = stringArr[1]
                if sepString.count > decimalPoint {
                    return false
                }
            }
            
            return true
            
        } else {
            return false
        }
    }


static func isNumber(_ value: String?) -> Bool {
        let nf = NumberFormatter()
        let isDecimal = nf.number(from: value ?? "") != nil
        return isDecimal
    }
-1

Swift 4, Xcode 9.2

This will remove the last entered decimal point:

let numberOfDecimalPoints = textField.text.components(separatedBy: ".").count - 1

if numberOfDecimalPoints > 1 {
    textField.deleteBackward()
}
Greg
  • 667
  • 8
  • 19