How to format a number(text) in UITextField when user is typing?

I have a converter which converts numbers. When user is typing a number from NumberPad keyboard into my cell textField I want it to be formatted and displayed in real time like:

1000 -> 1 000
10000 -> 10 000
1000000 -> 1 000 000
1000,77 -> 1 000,77

I know it should happen in textFieldDidChangeSelection method. Here is mine:

func textFieldDidChangeSelection(_ textField: UITextField) {
    numberFromTextField = Double(textField.text!)

    //This is for reload visible Rows in my tableView. Might not needed in my question's context.

    let activeTextFieldIndexPath = IndexPath(row: textField.tag, section: 0)
    var visibleIndexPaths = [IndexPath]()
    
    for indexPath in tableView.indexPathsForVisibleRows! {
        if indexPath != activeTextFieldIndexPath {
            visibleIndexPaths.append(indexPath)
        }
    }
    tableView.reloadRows(at: visibleIndexPaths, with: .none)
}

There I have a global variable numberFromTextField which I made because I need to use the Double version of textField.text in my separate calculation methods.

How can I implement above formatted behaviour and at the same time save numberFromTextField = Double(textField.text!) as I need it in my different calculations?

Answered by Claude31 in 703796022

logic is following:

     // --------------------- shouldChangeCharactersIn ------------------------------------------------
     //  Description: testField is formatted with correct separators if number
     //  Parameters
     //        textField                     : UITextField        testField only
     //        shouldChangeCharactersIn range: NSRange
     //        replacementString string      : String    The last inserted String
     // -------------------------------------------------------------------------------------------------

     func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

          let formatter = NumberFormatter()
          formatter.locale = Locale(identifier: "en_US")                    // Adapt to your case
          formatter.usesGroupingSeparator = true
          formatter.numberStyle = NumberFormatter.Style.decimal
          formatter.maximumFractionDigits = 6
          formatter.decimalSeparator = "."                                  // Adapt to your case
          formatter.groupingSeparator = ","                                 // Adapt to your case
          
          // The complete string if string were added at the end
         // Here we only insert figures at the end

          // Let us first remove extra groupingSeparator we may have introduced to find the number
          let completeString = textField.text!.replacingOccurrences(of: formatter.groupingSeparator, with: "") + string 

          var backSpace = false
          
          if let char = string.cString(using: String.Encoding.utf8) {
              let isBackSpace = strcmp(char, "\\b")
              if (isBackSpace == -92) {
                  backSpace = true
              }
          }
          if string == "" && backSpace {           // backspace inserts nothing, but we need to accept it.
              return true
          }
          if string == "-" && textField.text! == "" {  // Accept leading minus
              return true
          }

          guard let value = Double(completeString) else { return false } // No double ; We do not insert
          
          let formattedNumber = formatter.string(from: NSNumber(value: value)) ?? ""
          textField.text = formattedNumber // We update the textField, adding typed character
          
          return string == formatter.decimalSeparator // No need to insert the typed char: we've done just above, unless we just typed separator
     }

Use

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { }

to do the change on the fly.

Accepted Answer

logic is following:

     // --------------------- shouldChangeCharactersIn ------------------------------------------------
     //  Description: testField is formatted with correct separators if number
     //  Parameters
     //        textField                     : UITextField        testField only
     //        shouldChangeCharactersIn range: NSRange
     //        replacementString string      : String    The last inserted String
     // -------------------------------------------------------------------------------------------------

     func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

          let formatter = NumberFormatter()
          formatter.locale = Locale(identifier: "en_US")                    // Adapt to your case
          formatter.usesGroupingSeparator = true
          formatter.numberStyle = NumberFormatter.Style.decimal
          formatter.maximumFractionDigits = 6
          formatter.decimalSeparator = "."                                  // Adapt to your case
          formatter.groupingSeparator = ","                                 // Adapt to your case
          
          // The complete string if string were added at the end
         // Here we only insert figures at the end

          // Let us first remove extra groupingSeparator we may have introduced to find the number
          let completeString = textField.text!.replacingOccurrences(of: formatter.groupingSeparator, with: "") + string 

          var backSpace = false
          
          if let char = string.cString(using: String.Encoding.utf8) {
              let isBackSpace = strcmp(char, "\\b")
              if (isBackSpace == -92) {
                  backSpace = true
              }
          }
          if string == "" && backSpace {           // backspace inserts nothing, but we need to accept it.
              return true
          }
          if string == "-" && textField.text! == "" {  // Accept leading minus
              return true
          }

          guard let value = Double(completeString) else { return false } // No double ; We do not insert
          
          let formattedNumber = formatter.string(from: NSNumber(value: value)) ?? ""
          textField.text = formattedNumber // We update the textField, adding typed character
          
          return string == formatter.decimalSeparator // No need to insert the typed char: we've done just above, unless we just typed separator
     }

Thank you very much, Claude!

After playing with the code I found that this version is good for me:

 func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        
        let formatter = NumberFormatter()
        formatter.usesGroupingSeparator = true
        formatter.numberStyle = .decimal
        formatter.decimalSeparator = "."
        formatter.groupingSeparator = " "
        
 let completeString = textField.text!.replacingOccurrences(of: formatter.groupingSeparator, with: "") + string
        
        guard let value = Double(completeString) else { return false }
        numberFromTextField = value
        
        let formattedNumber = formatter.string(from: NSNumber(value: value)) ?? ""
        textField.text = formattedNumber
        
        if string.isEmpty {
            return true
        }
        
        return string == formatter.decimalSeparator
    }

But I have one problem:

while pressing backspace on keyboard the first press doesn't remove the last character in string, the second and the next presses do. I think the problem is in my if string.isEmpty { return true } line as I found out when you press the backspace it returns an empty string... Short gif with problem: https://cln.sh/OZOslI

Any thoughts what's the problem might be?

I tried out Claude's code and it did mostly a great job, but it had a few serious problems. It works well if the user is only appending characters and only deleting from the end, but if they move the cursor and add/delete from the middle, or select several characters and add/delete replacing a multi-character range, then the code doesn't behave properly and jumps the cursor to the end of the text every time.

This code is based on Claude's and is a little more convoluted, but it seems to handle all of the scenarios that I can come up with and it should maintain the user's cursor in the same place that it was before we changed the text in the textfield.

My code also limits the fractional portion to a max of 3 digits, but you can customize that if you want.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

   let oldText = textField.text ?? ""
   let oldTextNS = oldText as NSString
   let newText = oldTextNS.replacingCharacters(in: range, with: string)

   formatter.usesGroupingSeparator = true
   formatter.numberStyle = NumberFormatter.Style.decimal
   formatter.maximumFractionDigits = 6

   var wasBackspace = false

   if let char = string.cString(using: String.Encoding.utf8) {
      let compareToBackspace = strcmp(char, "\\b")
      if (compareToBackspace == -92) {
         wasBackspace = true
      }
   }

   let strippedNewText = newText.replacingOccurrences(of: formatter.groupingSeparator, with: "")

   if strippedNewText == "-" {
      // Accept leading minus
      return true
   }

   if strippedNewText.lengthOfBytes(using: .utf8) == 0 {
      return true

   } else {
      // limit entry of fractional digits to max 3 places. But in order to do that, we need to let it format
      // to more than 3 digits and then check if it is generating more than 3 digits so that we can tell
      // the textfield to reject the change.

      guard let value = Double(strippedNewText) else {
         // couldn't parse a valid double, so reject the insert.
         return false
      }

      let formattedNumber = formatter.string(from: NSNumber(value: value)) ?? ""
      let formattedNumberWithoutThousandsGroups = formattedNumber.replacingOccurrences(of: formatter.groupingSeparator, with: "")
      guard let decimal = Decimal(string: formattedNumberWithoutThousandsGroups) else {
         // couldn't parse a valid decimal, so reject the insert.
         return false
      }
      
      // significantFractionalDecimalDigits is a custom extension:
      //
      // extension Decimal {
      //     var significantFractionalDecimalDigits: Int {
      //         return max(-exponent, 0)
      //     }
      // }
      if decimal.significantFractionalDecimalDigits > 3 {
         // too many fractional digits, don't let them insert another
         return false
      }

      textField.text = formattedNumber

      // check the original text, count up the number of grouping characters
      // we had in the substring before the changed range
      var numGroupersBeforeChange = 0
      var numNumberCharsBeforeChange = 0
      var i = 0
      for index in oldText.indices {
         if i == range.location {
            break
         }
         i += 1
         let char = oldText[index]
         if String(char) == formatter.groupingSeparator {
            numGroupersBeforeChange += 1
         } else {
            numNumberCharsBeforeChange += 1
         }
      }

      // check the newly formatted text, count up the number of grouping characters
      // we have spanning the range of the string covered that is analogous to the old text
      // up to the changed ranged.
      // for example, if the old text was "123,456,789" and we insert a 0 between the 5 and 6...
      // the string before the change would be "123,45" and numgroupersbeforechange=1
      // the analogous string after the change would be "1,234,5" and numgroupersafterchange=2
      var numGroupersAfterChange = 0
      var numNumberCharsAfterChange = 0
      i = 0
      for index in formattedNumber.indices {
         let char = formattedNumber[index]
         if String(char) == formatter.groupingSeparator {
            numGroupersAfterChange += 1
         } else {
            numNumberCharsAfterChange += 1
         }
         if numNumberCharsBeforeChange == numNumberCharsAfterChange {
            break
         }
         i += 1
      }

      let numGroupersAdded = numGroupersAfterChange - numGroupersBeforeChange

      var cursorOffset = range.location + numGroupersAdded
      let numCharsAddedByUserChange = wasBackspace ? 0 : string.count
      cursorOffset += numCharsAddedByUserChange

      if let newPosition = textField.position(from: textField.beginningOfDocument, offset: cursorOffset) {
         let newSelectedRange = textField.textRange(from: newPosition, to: newPosition)
         textField.selectedTextRange = newSelectedRange
      }

      // No need to insert the typed char: we've done just above, unless we just typed separator
      return string == formatter.decimalSeparator
   }
}
How to format a number(text) in UITextField when user is typing?
 
 
Q