Reputation: 319
I have a custom textField's input view - it is a Numpad
style keyboard. Numpad
is using to add numbers and math symbols to a textField
.
I can't figure out how can I change a math symbol in a string if user already add one and wants to change it on another straight away. Here is an example of what I need:
Here is the code I use:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//number formatter
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.locale = .current
formatter.roundingMode = .down
//all possible math operation symbols user can add
let symbolsSet = Set(["+","-","x","/"])
var amountOfSymbols = 0
let numberString = textField.text ?? ""
guard let range = Range(range, in: numberString) else { return false }
let updatedString = numberString.replacingCharacters(in: range, with: string)
let correctDecimalString = updatedString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
let completeString = correctDecimalString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
//current math symbol user add
let symbol = symbolsSet.filter(completeString.contains).last ?? ""
//if user add math symbol to an empty string - do not insert
if string == symbol, numberString.count == 0 { return false }
//count how much math symbols string has. If more that one - do not insert, string can have only one
completeString.forEach { character in
if symbolsSet.contains(String(character)) {
amountOfSymbols += 1
}
}
if amountOfSymbols > 1 { return false }
//count how much decimals string has. If more that one - do not insert because it can have only one per number
let numbersArray = completeString.components(separatedBy: symbol)
for number in numbersArray {
let amountOfDecimalSigns = number.filter({$0 == "."}).count
if amountOfDecimalSigns > 1 { return false }
}
//create numbers from a string
guard let firstNumber = Double(String(numbersArray.first ?? "0")) else { return true }
guard let secondNumber = Double(String(numbersArray.last ?? "0")) else { return true }
//format numbers and turn them back to string
let firstFormattedNumber = formatter.string(for: firstNumber) ?? ""
let secondFormattedNumber = formatter.string(for: secondNumber) ?? ""
//assign formatted numbers to a textField
textField.text = completeString.contains(symbol) ? "\(firstFormattedNumber)\(symbol)\(secondFormattedNumber)" : "\(firstFormattedNumber)"
return string == formatter.decimalSeparator
}
The logic for me was to use textField.deleteBackwards()
method to delete an old one and add a new math symbol after, but with above code it doesn't work: it deletes symbol, but a new one doesn't appear - I should press again so new symbol can appear.
What should I do to change a math symbol in a string?
Upvotes: 1
Views: 874
Reputation: 3853
Inside the shouldChangeCharactersIn
delegate use a logic as follows.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var shouldChange : Bool = true
//current text visible in the text field
let numberString = (textField.text ?? "").replacingOccurrences(of: ",", with: "")
//last charachter in the text field
let lastCharachter = numberString.last
//get all the numbers displayed in the textfield by sperating them from symbols
let numbersArray = numberString.components(separatedBy: CharacterSet(charactersIn: "+-x/"))
if string == ""{
}
//stop entering a symbol in the first place
else if numberString == ""{
if !Character(string).isNumber{
shouldChange = false
}
}
//if a number is entered check for the decimal points
else if Character(string).isNumber{
if numbersArray.last!.contains(".") && numbersArray.last?.components(separatedBy: ".").last?.count == 2 && Character(string).isNumber{
shouldChange = false
}else{
var symbol : String?
for n in numberString{
if "+-/x".contains(n){
symbol = String(n)
break
}
}
if (symbol != nil){
textField.text = "\(numbersArray.first!.withCommas())\(symbol!)\((numbersArray.last! + string).withCommas())"
}else{
textField.text = "\((numbersArray.first! + string).withCommas())"
}
shouldChange = false
}
// if symbol is entered
}else{
if string == "."{
if !lastCharachter!.isNumber{
shouldChange = false
}
}
//if there are more than 1 numbers numbersArray, calculate the value
else if lastCharachter!.isNumber{
if numbersArray.count > 1{
let expression = NSExpression(format: numberString)
let answer = expression.expressionValue(with:nil, context: nil) as! Double
textField.text = "\(forTrailingZero(temp: answer).withCommas())\(string)"
shouldChange = false
}
}else{
//change the symbol
textField.text = "\(textField.text!.dropLast())\(string)"
shouldChange = false
}
}
return shouldChange
}
//use to remove trailing zeros
func forTrailingZero(temp: Double) -> String {
let tempVar = String(format: "%g", temp)
return tempVar
}
extension String {
func withCommas() -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.locale = .current
return numberFormatter.string(from: NSNumber(value: Double(self)!))!
}
}
There may be different ways of achieving the expected output.
This is another way you can achieve the above result.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 1
formatter.locale = .current
formatter.roundingMode = .down
let numberString = "\(textField.text ?? "")".replacingOccurrences(of: ",", with: "")
let lastCharachter = numberString.last
let numbersArray = numberString.components(separatedBy: CharacterSet(charactersIn: "+-x/"))
let amountOfDecimalSigns = "\(numbersArray.last!)\(string)".filter({$0 == "."}).count
if numberString.last == "." && string == "0"{
formatter.minimumFractionDigits = 1
}else{
formatter.minimumFractionDigits = 0
}
if string == ""{
return true
}
if numberString == ""{
if Character(string).isNumber{
textField.text = string
}else{
return false
}
}
else if amountOfDecimalSigns > 1{
return false
}
else if numbersArray.count > 1 {
var symbol = ""
for str in numberString{
if "+-/x".contains(str){
symbol = String(str)
break
}
}
if Character(string).isNumber{
textField.text = "\(formatter.string(for: Float("\(numbersArray.first!)")! as NSNumber)!)\(symbol)\(formatter.string(for: Float("\(numbersArray.last!)\(string)")! as NSNumber)!)"
}else if string == "."{
textField.text = "\(textField.text!)\(string)"
}
else{
if lastCharachter!.isNumber{
let expression = NSExpression(format: numberString)
let answer = expression.expressionValue(with:nil, context: nil)
textField.text = "\(formatter.string(from: answer as! NSNumber)!)\(string)"
}else if lastCharachter! == "."{
let expression = NSExpression(format: String(numberString.dropLast()))
let answer = expression.expressionValue(with:nil, context: nil)
textField.text = "\(formatter.string(from: answer as! NSNumber)!)\(string)"
}
else{
textField.text = "\(numberString.dropLast())\(string)"
}
}
}else{
if Character(string).isNumber{
textField.text = "\(formatter.string(for: Float("\(numbersArray.first!)\(string)")! as NSNumber)!)"
}else{
if lastCharachter!.isNumber{
textField.text = "\(textField.text!)\(string)"
}else{
textField.text = "\(numberString.dropLast())\(string)"
}
}
}
return false
}
Check the logic in both answers.
Upvotes: 1
Reputation: 2028
You should add a listener for your UITextView text change.
NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: myTextField, queue: OperationQueue.main) { [weak self] (notification) in
guard let self = self else { return }
guard let textField = notification.object as? UITextField else { return }
//here write a logic which verifies if the last character in the text is one of the mathematical symbols, and the one previous to that is also a math symbol, then you would replace the 'lastIndex - 1' with the last character
guard let text = textField.text, text.count > 2 else { return }
guard let lastChar = text.last else { return }
let previousChar: Character = Array(text)[text.count - 2]
//compare these two characters and make sure the 'previousChar' is a math symbol but not a number, if so - replace it with 'lastChar'
}
Upvotes: 0