skip to Main Content

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?

Test project on GitHub

2

Answers


  1. 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'
    }
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search