SwiftUI TextField数字实时分组输入及光标定位问题求助
解决方案:基于UIKit封装实现实时千分位格式化的TextField
SwiftUI原生TextField难以同时满足实时千分位格式化和光标准确定位的需求,最可靠的方案是封装UIKit的UITextField,通过UITextFieldDelegate精细控制文本处理逻辑和光标位置。
核心逻辑
- 输入过滤:自动保留数字字符,忽略非数字内容
- 实时格式化:每次文本变化时,对纯数字字符串执行千分位分组
- 光标校准:计算格式化前后的光标偏移量,修正光标位置,避免编辑中间内容时光标跳转到末尾
完整实现代码
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
相关产品推荐
相关产品推荐

