hkray
February 12, 2017, 8:21am
1
Use subSet approach
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
print("Current text: \(textField.text)")
print("Replacement text: <\(string)> ", terminator: "")
let allowedCharacterSet = CharacterSet(charactersIn: "0123456789.")
let replacementStringCharacterSet = CharacterSet(charactersIn: string)
if !replacementStringCharacterSet.isSubset(of: allowedCharacterSet) {
print("Rejected (Invalid Character)")
return false
}
let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
let replacementTextHasDecimalSeparator = string.range(of: ".")
if existingTextHasDecimalSeparator != nil,
replacementTextHasDecimalSeparator != nil {
print("Rejected (Already has decimal point)")
return false
} else {
print("Accepted")
return true
}
}
hkray
February 13, 2017, 10:47am
2
Another approach.
Let swift Double() to determine whether the input is valid or not.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
print("Current text: \(textField.text)")
print("Replacement text: <\(string)> at position \(range.location)")
let newText: String
if let oldText = textField.text {
let startIndex = oldText.index(oldText.startIndex, offsetBy: range.location)
let endIndex = oldText.index(startIndex, offsetBy: range.length)
let replacementRange = startIndex..<endIndex
newText = oldText.replacingCharacters(in: replacementRange, with: string)
} else {
newText = string
}
print("New text: \(newText) ", terminator: "")
if Double(newText) != nil || newText.isEmpty || newText == "-" || newText == "." {
print("Accepted")
return true
} else {
print("Rejected")
return false
}
}
hkray
February 14, 2017, 6:25am
3
Another approach.
Accept inputs like: 98.6, -27, .5, -.001
Reject inputs that are not in valid number format.
Changes:
No need for class ConversionViewController to conform to the protocol UITextFieldDelegate. Remove keyword UITextFieldDelegate in class declaration.
Add a variable textBackup to store the last acceptable input.
class ConversionViewController: UIViewController {
var textBackup = String()
Delete the function textField(: shouldChangeCharactersIn: replacementString:)
The job of checking valid input is now done by fahrenheitFieldEditingChanged( : )
@IBAction func fahrenheitFieldEditingChanged(_ textField: UITextField) {
if var text = textField.text {
text = text.trimmingCharacters(in: .whitespaces)
if let number = numberFormatter.number(from: text) {
fahrenheitValue = Measurement(value: number.doubleValue, unit: .fahrenheit)
} else {
// Cannot convert text to valid number format
if text.isEmpty || text == "." || text == "-" || text == "-." {
// Accept text. Adding further input may make it a valid number
fahrenheitValue = nil
} else {
// Reject text. Restore backup
text = textBackup
}
}
textField.text = text
textBackup = text
} else {
// textField.text == nil
fahrenheitValue = nil
}
}
Here is my solution:
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
let letterCharacters = NSCharacterSet.letters
let spaceCharacters = NSCharacterSet.whitespacesAndNewlines
let punctuationAndSpecialCharacters = CharacterSet.init(charactersIn: "!#$&@~()[];,<>/?*|'\'" )
let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
let replacementTextHasDecimalSeparator = string.range(of: ".")
let containLetterCharacter = string.rangeOfCharacter(from: letterCharacters)
let containSpacesAndNewLineCharacters = string.rangeOfCharacter(from: spaceCharacters)
let containPunctuationAndSpecialCharacters = string.rangeOfCharacter(from: punctuationAndSpecialCharacters)
if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
return false
} else if containLetterCharacter != nil {
return false
} else if containSpacesAndNewLineCharacters != nil {
return false
} else if containPunctuationAndSpecialCharacters != nil {
return false
} else {
return true
}
}
bchen
September 6, 2017, 8:03pm
5
the punctuationAndSpecialCharacters should contain the quote sign (").
let punctuationAndSpecialCharacters = CharacterSet.init(charactersIn: "\"!#$&@~()[];,<>/?*|'\'" )
1 Like
I came up with the same solution.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
let replacementTextHasDecimalSeparator = string.range(of: ".")
let numbers = CharacterSet.init(charactersIn: "0123456789.")
let thisLabel = CharacterSet.init(charactersIn: string)
if !numbers.isSuperset(of: thisLabel) {
return false
}
if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
return false
} else {
return true
}
}
}
3 Likes
ibex10
October 22, 2017, 4:15am
7
Is there a particular reason for calling the init method explicitly rather than letting the languages’s runtime system do it?
let numbers = CharacterSet (charactersIn: "0123456789.")
let thisLabel = CharacterSet (charactersIn: string)
1 Like
hkray
October 22, 2017, 4:47am
8
Just a shorthand.
“If you specify a type by name, you can access the type’s initializer without using an initializer expression.”
The Swift Programming Language (Swift 4) / Language Reference / Expressions / Postfix Expressions / Initializer Expression
Another approach - use regex!
Add a function to ConversionViewController to detect whether you have something other than a number or decimal point,
func doesStringContainSomethingEleseOtherThanNumbersOrAPeriod(_ string: String) -> Bool {
let regexPattern: String = "^[1-9]\\d*(\\.\\d+)?$"
if let _ = string.range(of:regexPattern, options: .regularExpression) {
return false
} else {
return true
}
}
Then in the ConversionViewController, make this function call check before the existing check,
func textField(_ textField: UITextField, shouldChangeCharactersIn range:NSRange, replacementString string: String) -> Bool {
let existingTextHasDecimalSeparator = textField.text?.range(of: “.”)
let replacementTextHasDecimalSeparator = string.range(of: “.”)
if doesStringContainSomethingEleseOtherThanNumbersOrAPeriod(string) { return false }
if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
return false
} else {
return true
}
}
Looking through all these solutions it’s clear that the best way to solve this challenge is to use Int failable initializer from a string.
There’re too much hardcoding like:
let allowedCharacterSet = CharacterSet(charactersIn: “0123456789.”)
better:
let allowedCharacterSet = CharacterSet.decimalDigits
As for “.” you already have two full statement without a need to insert it into the set.
decimalDigits does not take into consideration “.”
therefore hardcoding is necessary to allow for “.” input
Another version. Using CharacterSet to define valid characters, including a minus sign to type negative numbers. Then checks so that there is only one decimal separator, only legal characters and if there is a minus sign it has to be at position 0.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let existingStringHasDecimalSeparator = textField.text?.range(of: ".")
let replacementTextHasDecimalOperator = string.range(of: ".")
let validCharacters = CharacterSet(charactersIn: ".-0123456789")
let replacementCharacter = CharacterSet(charactersIn: string)
if (existingStringHasDecimalSeparator != nil && replacementTextHasDecimalOperator != nil) || !validCharacters.isSuperset(of: replacementCharacter) || (string == "-" && range.location != 0) {
return false
} else {
return true
}
}
1 Like