You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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
    }
}

关键说明

  1. 格式化逻辑:通过反转整数部分实现千位分组,比正向遍历更简洁高效,避免了复杂的索引计算。
  2. 光标同步:始终基于纯数字文本的位置计算光标,彻底解决了分隔符导致的光标错位问题。
  3. 边界处理:覆盖了开头多0、删除后整数为空、小数部分补0等场景,保证输入体验流畅自然。

内容的提问来源于stack exchange,提问作者Swifty Swift

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:25:44