如何在UITextField输入数字时添加千位分隔符(货币输入场景)
解决UITextField整数部分千位分隔符问题
我看了你的代码,小数部分的限制逻辑已经做得很扎实了,现在要给整数部分加上千位分隔符,核心是实现一个数字格式化函数,同时要处理好光标位置的同步(因为添加分隔符后字符串长度会变化,光标容易错位)。下面是具体的解决方案:
步骤1:添加数字格式化函数
这个函数会先清理现有分隔符,对整数部分进行千位分组,再拼接小数部分,确保格式统一:
func formatNumberWithThousandSeparator(_ text: String) -> String { // 先移除所有已有的千位分隔符,避免重复添加 let cleanedText = text.replacingOccurrences(of: THOUSAND_SEPERATOR, with: "") let components = cleanedText.components(separatedBy: DECIMAL_SEPERATOR) // 处理整数部分的千位分组 var integerPart = components[0] if integerPart.isEmpty || integerPart == "0" { integerPart = "0" } else { // 反转字符串后每3位插入分隔符,再反转回来,这种方式更易实现 let reversedInteger = String(integerPart.reversed()) var formattedInteger = "" for (index, char) in reversedInteger.enumerated() { if index > 0 && index % 3 == 0 { formattedInteger.append(THOUSAND_SEPERATOR) } formattedInteger.append(char) } integerPart = String(formattedInteger.reversed()) } // 处理小数部分,确保不超过指定位数 if components.count > 1 { var decimalPart = components[1] if decimalPart.count > DECIMAL_DIGITS { decimalPart = String(decimalPart.prefix(DECIMAL_DIGITS)) } return "\(integerPart)\(DECIMAL_SEPERATOR)\(decimalPart)" } else { return integerPart } }
步骤2:添加光标位置计算函数
格式化后字符串长度会变化,我们需要根据无分隔符的纯数字位置,计算出带分隔符字符串中的光标位置,避免光标错位:
func calculateCursorPosition(in formattedText: String, cleanTextPosition: Int) -> Int { var currentCleanIndex = 0 for (index, char) in formattedText.enumerated() { if char != Character(THOUSAND_SEPERATOR) { if currentCleanIndex == cleanTextPosition { return index } currentCleanIndex += 1 } } return formattedText.count }
步骤3:修改现有函数逻辑
3.1 更新textFieldShouldAddNumber函数
去掉原有的dots参数,改用纯数字文本处理,添加数字后自动格式化并调整光标:
func textFieldShouldAddNumber(text: String, range: NSRange, replacement: String) -> Bool{ let cleanedText = text.replacingOccurrences(of: THOUSAND_SEPERATOR, with: "") let nsstring = NSString(string: cleanedText) let decimalRange = nsstring.range(of: DECIMAL_SEPERATOR) if range.location > decimalRange.location { // 限制小数部分位数 let parts = cleanedText.components(separatedBy: DECIMAL_SEPERATOR) if parts.count > 1 && parts[1].count >= DECIMAL_DIGITS { return false } // 生成新文本并格式化 let newCleanedText = nsstring.replacingCharacters(in: range, with: replacement) as String let formattedText = formatNumberWithThousandSeparator(newCleanedText) textField.text = formattedText // 调整光标到正确位置 let cursorInClean = range.location + 1 let newCursorPos = calculateCursorPosition(in: formattedText, cleanTextPosition: cursorInClean) if let pos = textField.position(from: textField.beginningOfDocument, offset: newCursorPos) { textField.selectedTextRange = textField.textRange(from: pos, to: pos) } return false } else if range.location < decimalRange.location { // 避免整数部分开头出现多个0 var newCleanedText = nsstring.replacingCharacters(in: range, with: replacement) as String if newCleanedText.hasPrefix("0") && newCleanedText.count > 1 && !newCleanedText.contains(DECIMAL_SEPERATOR) { newCleanedText = String(newCleanedText.dropFirst()) } // 格式化并调整光标 let formattedText = formatNumberWithThousandSeparator(newCleanedText) textField.text = formattedText let cursorInClean = newCleanedText.count == 1 ? 1 : (range.location + 1) let newCursorPos = calculateCursorPosition(in: formattedText, cleanTextPosition: cursorInClean) if let pos = textField.position(from: textField.beginningOfDocument, offset: newCursorPos) { textField.selectedTextRange = textField.textRange(from: pos, to: pos) } return false } return true }
3.2 修改shouldChangeCharactersInRange函数
处理删除操作时也要自动格式化,并调整光标位置,同时移除原有的dots变量:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if string == "" { // 处理删除操作 guard var currentText = textField.text else { return true } let cleanedText = currentText.replacingOccurrences(of: THOUSAND_SEPERATOR, with: "") let nsCleaned = NSString(string: cleanedText) var newCleanedText = nsCleaned.replacingCharacters(in: range, with: "") as String // 删除后整数部分为空时,自动补0 let components = newCleanedText.components(separatedBy: DECIMAL_SEPERATOR) if components[0].isEmpty { newCleanedText = "0\(components.count > 1 ? "\(DECIMAL_SEPERATOR)\(components[1])" : "")" } // 格式化并调整光标 let formattedText = formatNumberWithThousandSeparator(newCleanedText) textField.text = formattedText let originalCleanPos = range.location - (currentText.count - cleanedText.count) let newCleanPos = max(0, originalCleanPos - 1) let newCursorPos = calculateCursorPosition(in: formattedText, cleanTextPosition: newCleanPos) if let pos = textField.position(from: textField.beginningOfDocument, offset: newCursorPos) { textField.selectedTextRange = textField.textRange(from: pos, to: pos) } return false } if DIGITS.contains(string){ return textFieldShouldAddNumber(text: textField.text!, range: range, replacement: string) } if string == DECIMAL_SEPERATOR || string == THOUSAND_SEPERATOR{ if textField.text!.contains(DECIMAL_SEPERATOR){ return false } textFieldTriggerDecimalTyped() return false } return false }
3.3 更新textFieldTriggerDone函数
结束编辑时,确保小数部分补全,同时整数部分带上千位分隔符:
func textFieldTriggerDone(){ guard var currentText = textField.text else { return } if !currentText.contains(DECIMAL_SEPERATOR){ currentText += "\(DECIMAL_SEPERATOR)\(DECIMAL_DIGITS_DEFAULT_STRING)" } else { let components = currentText.components(separatedBy: DECIMAL_SEPERATOR) if components.count > 1 { let decimalPart = components[1] if decimalPart.count < DECIMAL_DIGITS { let padding = String(repeating: "0", count: DECIMAL_DIGITS - decimalPart.count) currentText = "\(components[0])\(DECIMAL_SEPERATOR)\(decimalPart)\(padding)" } } } textField.text = formatNumberWithThousandSeparator(currentText) }
完整修改后的代码
整合所有逻辑,加上必要的扩展:
import UIKit extension String { // 保留原有的统计分隔符方法(若其他场景需要) func occurancesOf(_ substring: String) -> Int { return components(separatedBy: substring).count - 1 } } class ViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var textField: UITextField! let DIGITS = ["0","1","2","3","4","5","6","7","8","9"] let DECIMAL_SEPERATOR = "," let THOUSAND_SEPERATOR = "." let DECIMAL_DIGITS = 2 let DECIMAL_DIGITS_DEFAULT_STRING = "00" override func viewDidLoad() { super.viewDidLoad() textField.delegate = self textField.text = formatNumberWithThousandSeparator("0\(DECIMAL_SEPERATOR)\(DECIMAL_DIGITS_DEFAULT_STRING)") } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } // MARK: - 数字格式化与光标处理 func formatNumberWithThousandSeparator(_ text: String) -> String { let cleanedText = text.replacingOccurrences(of: THOUSAND_SEPERATOR, with: "") let components = cleanedText.components(separatedBy: DECIMAL_SEPERATOR) var integerPart = components[0] if integerPart.isEmpty || integerPart == "0" { integerPart = "0" } else { let reversedInteger = String(integerPart.reversed()) var formattedInteger = "" for (index, char) in reversedInteger.enumerated() { if index > 0 && index % 3 == 0 { formattedInteger.append(THOUSAND_SEPERATOR) } formattedInteger.append(char) } integerPart = String(formattedInteger.reversed()) } if components.count > 1 { var decimalPart = components[1] if decimalPart.count > DECIMAL_DIGITS { decimalPart = String(decimalPart.prefix(DECIMAL_DIGITS)) } return "\(integerPart)\(DECIMAL_SEPERATOR)\(decimalPart)" } else { return integerPart } } func calculateCursorPosition(in formattedText: String, cleanTextPosition: Int) -> Int { var currentCleanIndex = 0 for (index, char) in formattedText.enumerated() { if char != Character(THOUSAND_SEPERATOR) { if currentCleanIndex == cleanTextPosition { return index } currentCleanIndex += 1 } } return formattedText.count } // MARK: - TextField 相关方法 func textFieldTriggerDone(){ guard var currentText = textField.text else { return } if !currentText.contains(DECIMAL_SEPERATOR){ currentText += "\(DECIMAL_SEPERATOR)\(DECIMAL_DIGITS_DEFAULT_STRING)" } else { let components = currentText.components(separatedBy: DECIMAL_SEPERATOR) if components.count > 1 { let decimalPart = components[1] if decimalPart.count < DECIMAL_DIGITS { let padding = String(repeating: "0", count: DECIMAL_DIGITS - decimalPart.count) currentText = "\(components[0])\(DECIMAL_SEPERATOR)\(decimalPart)\(padding)" } } } textField.text = formatNumberWithThousandSeparator(currentText) } func textFieldTriggerDecimalTyped(){ textField.text = textField.text! + DECIMAL_SEPERATOR } func textFieldShouldAddNumber(text: String, range: NSRange, replacement: String) -> Bool{ let cleanedText = text.replacingOccurrences(of: THOUSAND_SEPERATOR, with: "") let nsstring = NSString(string: cleanedText) let decimalRange = nsstring.range(of: DECIMAL_SEPERATOR) if range.location > decimalRange.location { let parts = cleanedText.components(separatedBy: DECIMAL_SEPERATOR) if parts.count > 1 && parts[1].count >= DECIMAL_DIGITS { return false } let newCleanedText = nsstring.replacingCharacters(in: range, with: replacement) as String let formattedText = formatNumberWithThousandSeparator(newCleanedText) textField.text = formattedText let cursorInClean = range.location + 1 let newCursorPos = calculateCursorPosition(in: formattedText, cleanTextPosition: cursorInClean) if let pos = textField.position(from: textField.beginningOfDocument, offset: newCursorPos) { textField.selectedTextRange = textField.textRange(from: pos, to: pos) } return false } else if range.location < decimalRange.location { var newCleanedText = nsstring.replacingCharacters(in: range, with: replacement) as String if newCleanedText.hasPrefix("0") && newCleanedText.count > 1 && !newCleanedText.contains(DECIMAL_SEPERATOR) { newCleanedText = String(newCleanedText.dropFirst()) } let formattedText = formatNumberWithThousandSeparator(newCleanedText) textField.text = formattedText let cursorInClean = newCleanedText.count == 1 ? 1 : (range.location + 1) let newCursorPos = calculateCursorPosition(in: formattedText, cleanTextPosition: cursorInClean) if let pos = textField.position(from: textField.beginningOfDocument, offset: newCursorPos) { textField.selectedTextRange = textField.textRange(from: pos, to: pos) } return false } return true } func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } func textFieldDidEndEditing(_ textField: UITextField) { textFieldTriggerDone() } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if string == "" { guard var currentText = textField.text else { return true } let cleanedText = currentText.replacingOccurrences(of: THOUSAND_SEPERATOR, with: "") let nsCleaned = NSString(string: cleanedText) var newCleanedText = nsCleaned.replacingCharacters(in: range, with: "") as String let components = newCleanedText.components(separatedBy: DECIMAL_SEPERATOR) if components[0].isEmpty { newCleanedText = "0\(components.count > 1 ? "\(DECIMAL_SEPERATOR)\(components[1])" : "")" } let formattedText = formatNumberWithThousandSeparator(newCleanedText) textField.text = formattedText let originalCleanPos = range.location - (currentText.count - cleanedText.count) let newCleanPos = max(0, originalCleanPos - 1) let newCursorPos = calculateCursorPosition(in: formattedText, cleanTextPosition: newCleanPos) if let pos = textField.position(from: textField.beginningOfDocument, offset: newCursorPos) { textField.selectedTextRange = textField.textRange(from: pos, to: pos) } return false } if DIGITS.contains(string){ return textFieldShouldAddNumber(text: textField.text!, range: range, replacement: string) } if string == DECIMAL_SEPERATOR || string == THOUSAND_SEPERATOR{ if textField.text!.contains(DECIMAL_SEPERATOR){ return false } textFieldTriggerDecimalTyped() return false } return false } }
关键说明
- 格式化逻辑:通过反转整数部分实现千位分组,比正向遍历更简洁高效,避免了复杂的索引计算。
- 光标同步:始终基于纯数字文本的位置计算光标,彻底解决了分隔符导致的光标错位问题。
- 边界处理:覆盖了开头多0、删除后整数为空、小数部分补0等场景,保证输入体验流畅自然。
内容的提问来源于stack exchange,提问作者Swifty Swift
相关产品推荐
相关产品推荐

