How to replace a specific character in a string inside a UITextField?

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: https://i.stack.imgur.com/IVGId.gif

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?

Test project on GitHub

Replies

I suspect problem to be here:

let symbol = symbolsSet.filter(completeString.contains).last ?? ""

Could you print some log and tell what you get when you type second symbol ?:

print("symbol", symbol)
print("string", string)
print("updatedString", updatedString)
print("correctDecimalString", correctDecimalString)
let completeString = correctDecimalString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
print("completeString", completeString)
  • Hi Claude. Sure. Here is a gif with behaviour: https://share.cleanshot.com/I9S2Nz, Here is the screenshot with log: https://share.cleanshot.com/uI1ior When I type second symbol nothing happens...

  • Here is what happened if place prints before check for how much math symbols completeString has: https://share.cleanshot.com/r74s3g and screenshot: https://share.cleanshot.com/5bmETE You can see string can have 2 math symbols, but appear only one because I have a block with if amountOfSymbols > 1 { return false } So I need somehow to change that previous math symbol which user see, on the recent one user typed and assign it to textField...

  • Dear Claude, I also posted some update in a separate reply. Please take a look

Add a Comment

@Claude31

The closer I can get is to use the code:


var completeString = correctDecimalString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")

 if amountOfSymbols > 1 {

    let newSymbol = completeString.last?.description ?? ""
    completeString.removeAll { symbolsSet.contains(String($0))}
    textField.text = completeString+newSymbol

    return false

 }

But then If I'll add math symbol again it will change the first number which I want to avoid: https://share.cleanshot.com/tmFiAr

That's a usual problem with shouldChangeCharactersIn. The string tou use is not updated yet as you expect.

So please first answer my question and tell what you get in console, precisely, and what you typed when problem occurs.

print("symbol", symbol)
print("string", string)
print("updatedString", updatedString)
print("correctDecimalString", correctDecimalString)
let completeString = correctDecimalString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
print("completeString", completeString)

As for your last try, you should not change textField.text in this func, but the textField IBOutlet that you modify.

  • Claude, thank you for the reply. Posted all console output in a new post. Thank you!

  • Dear Claude, did you have time to check the console output, which I post in a separate reply? Thank you! I just really stuck on it

  • Any reply?

Add a Comment

@Claude31

Here is what I get in console:

  1. Put print statements before check on how much math symbols string has:
 func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

//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 }

 print("symbol", symbol)
 print("string", string)
 print("updatedString", updatedString)
 print("correctDecimalString", correctDecimalString)
 print("completeString", completeString)

//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 }
}

Here is the console output after each character I type:

Press 2

symbol 
string 2
updatedString 2
correctDecimalString 2
completeString 2

Press +

symbol +
string +
updatedString 2+
correctDecimalString 2+
completeString 2+

Press -

symbol +
string -
updatedString 2+-
correctDecimalString 2+-
completeString 2+-

2.Put print statements after check on how much math symbols string has:

if amountOfSymbols > 1 { return false }

  print("symbol", symbol)
  print("string", string)
  print("updatedString", updatedString)
  print("correctDecimalString", correctDecimalString)
  print("completeString", completeString)

Here is the console output after each character I type:

Press 2

symbol 
string 2
updatedString 2
correctDecimalString 2
completeString 2

Press +

symbol +
string +
updatedString 2+
correctDecimalString 2+
completeString 2+

Press -

Nothing is printed