Only allow one decimal for each number in UITextView (Swift 4) - ios

I have a UITextView into which I type a series of space-delimited numbers (for example: "12.3 30 22.7 19.23 15 8.5 11").
The UITextView may contain multiple decimal points, but I want to ensure that no individual number contains more than one decimal place.
All the solutions I have found only restrict the entire UITextView to a single decimal, not each number.
How can I do this?

You can try something like this in the UITextView delegate func shouldEndEditing..
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
guard let numbers = textView.text else {
return true
}
var hasMultipleDecimalPlaces: Bool = false
let numbersAsList = numbers.split(separator: " ")
for number in numbersAsList {
let splitAtDecimal = number.split(separator: ".")
if splitAtDecimal.count > 2 {
if splitAtDecimal[1].count > 2 {
// This is what you want to prevent so break early and return false
hasMultipleDecimalPlaces = true
break
}
}
}
return !hasMultipleDecimalPlaces
}
This will prevent the user from finishing editing the textview. It can always be used in a function like shouldReplaceCharactersIn

I think you should use this delegate textField(_:shouldChangeCharactersIn:replacementString:) and by your self check every single number and check if there was already point or not. And return false or true.

Try a regular expression in the text view delegate method:
let regex = try! NSRegularExpression(pattern: "^\\s*((-?)(\\d+\\.?|\\d*\\.\\d+))?(\\s+(-?)(\\d+\\.?|\\d*\\.\\d+))*\\s*$")
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Compute the value of the text view after the text is inserted.
let finalText = (textView.text as NSString).replacingCharacters(in: range, with: text) as NSString
// Check against a regular expression.
return regex.firstMatch(in: finalText as String, range: NSRange(location: 0, length: finalText.length)) != nil
}
You are going to have to experiment with the behavior that you want:
https://regex101.com

Final result:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// only allow valid characters
let validCharacters = " -0123456789.,|\t\n"
guard CharacterSet(charactersIn: validCharacters).isSuperset(of: CharacterSet(charactersIn: text)) else {
return false
}
let oldText = txtViewDataInput.text!
let r = Range(range, in: oldText)
let text = oldText.replacingCharacters(in: r!, with: text)
debugPrint("text = \(text)")
let whiteSpace = " ,|\t\n"
let separators = NSCharacterSet(charactersIn: whiteSpace)
let tempArray = text.components(separatedBy: separators as CharacterSet)
// do not allow more than one decimal point in a number
for number in tempArray {
let splitAtDecimal = number.filter { $0 == "." }.count
if splitAtDecimal > 1 {
return false
}
}
return true
}

Related

Swift 4: How to set only one word per row in an UITextView and create an array with each row

I created an UITextView and I want to have only one word per row, so that each time I press the space dash it actually returns the text. How can I make the space button return instead of actually spacing the text? Plus, is there a way to record each word in order to create a custom array?
This is my code right now:
#IBAction weak var wordView : UITextView!
#IBLabel weak var label : UILabel!
var words : [String] = []
override func viewDidLoad() {
super.viewDidLoad()
let endEditingTapGesture = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing(_:)))
endEditingTapGesture.cancelsTouchesInView = false
view.addGestureRecognizer(endEditingTapGesture)
}
#IBAction func button(_ sender: Any) {
getArray()
}
func getArray() {
for _ in words {
words.append(wordView.text)
}
}
The button should add words to the array each time I press it... I'm not sure this is the best solution... any help?
You can use delegate if you're using taking input from same.
here is sample code.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let oldText = NSString(format: "%#", textView.text)
var newText = oldText.replacingCharacters(in: range, with: text)
newText = newText.replacingOccurrences(of: " ", with: "\n")
newText = newText.replacingOccurrences(of: "-", with: "\n")
textView.text = newText
let myWordArr = textView.text.components(separatedBy: "\n")
print(myWordArr)
return false
}
don't forget to set delegate.
try this to replace space with new line use textView delegate method as below,
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
//checks if new text is white space
if (text == " ") {
if (textView.text?.characters.last == "\n") {
// this will prevent multiple new lines
return false
}
let newText = (textView.text as NSString).replacingCharacters(in: range, with: "\n")
textView.text = newText
return false
}
return true
}
to get the array of all words
let allWords = yourTextView.text.components(separatedBy: "\n")

How to get the Character that is before the cursor in a UITextView?

Let's say I have the following UITextView object:
var textView = UITextView()
textView.text = "Hello World!"
Now let's say I don't want to allow the user to delete the "W" character while editing it. How could I know which character is before the cursor (or selected by it)?
I'm looking for something that would work like this:
if textView.characterBeforeCursor() != "W" {
textView.deleteBackward()
}
or... (when the user selects the "W" character):
if textView.selectedTextContains("W") == false {
textView.deleteBackward()
}
What approach should I use to accomplish this?
Here's an idea, not fully tested, but seems to work... Just grab the character about to be acted upon and block backspace if its the target... Also with regard to selection of text, if the selection contains the target at all, we block new text.
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
self.textView.delegate = self
// Do any additional setup after loading the view, typically from a nib.
}
func characterBeforeCursor() -> String? {
// get the cursor position
if let cursorRange = textView.selectedTextRange {
// get the position one character before the cursor start position
if let newPosition = textView.position(from: cursorRange.start, offset: -1) {
let range = textView.textRange(from: newPosition, to: cursorRange.start)
return textView.text(in: range!)
}
}
return nil
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (characterBeforeCursor() == "W") {
let char = text.cString(using: String.Encoding.utf8)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
return false
}
return true
}
else {
if let range = textView.selectedTextRange {
let selectedText = textView.text(in: range)
if (selectedText!.contains("W")) {
return false
}
}
return true
}
}
}
This should do it:
let forbiddenLetter = "W"
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let txt = textView.text, let txtRange = Range(range, in: txt) else {
return false
}
let subString: Substring = txt[txtRange]
return !subString.contains(forbiddenLetter)
}
In the code above let txt = textView.text is just for simplicity, we could keep force-unwrapping textView.text! since the .text property is designed never returns nil for a non-nil UITextView.
By let txtRange = Range(range, in: txt) we get a variable of type Range<String.Index> instead of the vanilla NSRange that range is. By doing so we can get the Substring of txt that the textView is about to change.
Finally, the result of checking whether or not the subString contains the forbiddenLetter, is returned.
This snippet would prevent deleting W by using:
Backspace key ⌫
Deleting selection
Pasting over selection
Autocorrect (from the popup)

Limiting user input to a valid decimal number less than 99999.99 in Swift [duplicate]

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
}

Deleting word by word instead of character by character in textview ios

I have been trying to delete word by word that means If I have a sentence like "Hello, this #Sivajee is sample text", each time when I'm deleting 'e' letter in #Sivajee it should delete entire word #Sivajee from the sentence. I really have no clue about this.
Here is my code.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let newText = (textView.text as NSString).replacingCharacters(in: range, with: text)
let substring = (textView.text as NSString).substring(with: range)
//Tagging users
if text == "#" || substring == "#" || (isTagging && text == " "){
isTagging = !isTagging
searchTerm.removeAll()
tagSearchedUsers.removeAll()
tableView.reloadData()
}
else if isTagging {
searchTerm.append(text)
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.getFriendsSearchResults), object: nil)
self.perform(#selector(self.getFriendsSearchResults), with: nil, afterDelay: 0.5)
}
return true;
}
On a whole, my idea is to tag someone while typing & deleting when a user uses backspace. Your input will be more valuable for me.
Edit:
The swift equivalent of proposed solution from comments not working
if (string == "") {
let selectedRange: UITextRange? = textField.selectedTextRange
let cursorOffset: Int = textField.offset(from: 0, to: (selectedRange?.start)!)
let text: String? = textField.text
let substring: String? = (text as? NSString)?.substring(to: cursorOffset)
let lastWord = substring?.components(separatedBy: " ")?.last as? String
if lastWord?.hasPrefix("#") ?? false {
// Delete word
textField.text = self.textField?.text()?.replacingOccurrences(of: lastWord!, with: "")
return false
}
}
Swift 4:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
{
if text == ""
{
if let selectedRange = textView.selectedTextRange
{
let cursorOffset = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
if let myText = textView.text
{
let index = myText.index(myText.startIndex, offsetBy: cursorOffset)
let substring = myText[..<index]
if let lastword = substring.components(separatedBy: " ").last
{
if lastword.hasPrefix("#")
{
//Check complete word
let completeLastword = myText.components(separatedBy: " ").filter{$0.contains(lastword)}.last
textView.text = myText.replacingOccurrences(of: completeLastword!, with: "")
return false
}
}
}
}
}
return true
}
Here is the Swift version of the proposed answer by commenters.
if (text == "") {
let selectedRange: UITextRange? = textView.selectedTextRange
let cursorOffset: Int = textView.offset(from: textView.beginningOfDocument, to: (selectedRange?.start)!)
let text: String? = textView.text
let substring: String? = (text as NSString?)?.substring(to: cursorOffset)
let lastWord = substring?.components(separatedBy: " ").last
print(lastWord)
if lastWord?.hasPrefix("#") ?? false {
// Delete word
textView.text = textView.text?.replacingOccurrences(of: lastWord!, with: "")
return false
}
}
But still it has draw back of, when deleting word from the middle it will delete only portion of it. Not entire word. That means, If I have word #Sivajee, If I put my cursor at letter 'V' and backspace, it will only delete #Siv. Not ajee also.

Limiting user input to a valid decimal number in Swift

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
}

Resources