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

SwiftUI TextField数字实时分组输入及光标定位问题求助

解决方案:基于UIKit封装实现实时千分位格式化的TextField

SwiftUI原生TextField难以同时满足实时千分位格式化和光标准确定位的需求,最可靠的方案是封装UIKit的UITextField,通过UITextFieldDelegate精细控制文本处理逻辑和光标位置。

核心逻辑

  1. 输入过滤:自动保留数字字符,忽略非数字内容
  2. 实时格式化:每次文本变化时,对纯数字字符串执行千分位分组
  3. 光标校准:计算格式化前后的光标偏移量,修正光标位置,避免编辑中间内容时光标跳转到末尾

完整实现代码

import SwiftUI
import UIKit

struct FormattedNumberTextField: UIViewRepresentable {
    @Binding var text: String
    let placeholder: String
    
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.placeholder = placeholder
        textField.keyboardType = .numberPad
        textField.delegate = context.coordinator
        textField.addTarget(context.coordinator, action: #selector(Coordinator.textChanged), for: .editingChanged)
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        // 避免循环触发格式化,仅当外部文本与TextField内容不一致时更新
        if uiView.text != text {
            uiView.text = text
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, UITextFieldDelegate {
        let parent: FormattedNumberTextField
        
        init(parent: FormattedNumberTextField) {
            self.parent = parent
        }
        
        @objc func textChanged(_ textField: UITextField) {
            guard let inputText = textField.text else { return }
            
            // 记录格式化前的光标位置
            let cursorOffset = textField.offset(from: textField.beginningOfDocument, to: textField.selectedTextRange?.start ?? textField.beginningOfDocument)
            
            // 提取纯数字内容
            let digits = inputText.filter { $0.isNumber }
            guard !digits.isEmpty else {
                parent.text = ""
                return
            }
            
            // 执行千分位格式化
            let formattedText = formatDigitsToThousandSeparator(digits: digits)
            
            // 计算格式化后的光标位置
            let newCursorOffset = calculateCursorOffset(originalOffset: cursorOffset, originalText: inputText, formattedText: formattedText)
            
            // 更新绑定文本
            parent.text = formattedText
            
            // 异步恢复光标位置,避免UI更新冲突
            DispatchQueue.main.async {
                if let newPosition = textField.position(from: textField.beginningOfDocument, offset: newCursorOffset) {
                    textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
                }
            }
        }
        
        private func formatDigitsToThousandSeparator(digits: String) -> String {
            var formatted = ""
            let reversedDigits = digits.reversed()
            
            for (index, char) in reversedDigits.enumerated() {
                if index != 0 && index % 3 == 0 {
                    formatted.append(",")
                }
                formatted.append(char)
            }
            return formatted.reversed()
        }
        
        private func calculateCursorOffset(originalOffset: Int, originalText: String, formattedText: String) -> Int {
            // 统计原文本光标前的逗号数量
            let originalCommas = originalText.prefix(originalOffset).filter { $0 == "," }.count
            // 统计原文本光标前的数字数量
            let digitsBeforeCursor = originalText.prefix(originalOffset).filter { $0.isNumber }.count
            // 统计格式化后文本中,对应数字位置前的逗号数量
            let formattedCommas = formattedText.prefix { char in
                if char.isNumber {
                    return digitsBeforeCursor > 0
                }
                return true
            }.filter { $0 == "," }.count
            
            // 计算新光标位置:原偏移 - 原逗号数 + 新逗号数
            return originalOffset - originalCommas + formattedCommas
        }
    }
}

// 使用示例
struct ContentView: View {
    @State private var numberText = ""
    
    var body: some View {
        FormattedNumberTextField(text: $numberText, placeholder: "输入数字")
            .padding()
            .border(Color.gray)
    }
}

关键细节说明

  • 实时响应:通过editingChanged事件监听输入变化,每一次按键都立即执行格式化
  • 光标校准:calculateCursorOffset方法根据原光标位置前后的逗号数量差,精准计算格式化后的光标位置,确保编辑体验流畅
  • 避免循环更新:updateUIView中添加内容一致性判断,防止外部文本更新与内部格式化触发循环

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 11:02:10