SwiftUI自定义信用卡格式TextField:粘贴/初始值无法自动格式化
信用卡格式TextField初始值/粘贴自动格式化问题解决
问题
通过UIViewRepresentable实现信用卡格式(xxxx xxxx xxxx xxxx)的自定义TextField时,设置初始值(如12345)或粘贴内容后,输入框不会自动格式化为1234 5,必须手动输入一个字符才会触发格式化逻辑。
解决方案
核心是在updateUIView中主动触发格式化逻辑,同时避免因绑定值更新导致的循环刷新。具体修改如下:
1. 抽离格式化逻辑
在Coordinator中添加独立的格式化方法,方便在初始化、更新时复用:
extension CreditCardTextField.Coordinator { // 格式化字符串为信用卡格式 func formatCardNumber(_ input: String) -> String { var cursorPosition = 0 let digitsOnly = removeNonDigits(string: input, andPreserveCursorPosition: &cursorPosition) guard digitsOnly.count <= 16 else { return input } return insertCreditCardSpaces(digitsOnly, preserveCursorPosition: &cursorPosition) } }
2. 修改updateUIView方法
在updateUIView中,先对绑定的number进行格式化,对比后再更新TextField内容,避免循环触发:
public func updateUIView(_ uiView: UITextField, context: Context) { let formattedNumber = context.coordinator.formatCardNumber(number) // 仅当格式化后的内容与当前TextField文本不一致时才更新,避免循环刷新 if uiView.text != formattedNumber { uiView.text = formattedNumber number = formattedNumber } }
3. 初始化时触发格式化
在makeUIView中,初始化TextField时直接设置格式化后的初始值:
public func makeUIView(context: Context) -> UITextField { let view = UITextField() view.addTarget(context.coordinator, action: #selector(Coordinator.reformatAsCardNumber), for: .editingChanged) view.delegate = context.coordinator // 初始化时设置格式化后的初始值 view.text = context.coordinator.formatCardNumber(number) return view }
修改后的完整代码
ContentView(无变化)
struct ContentView: View { @State var cardNumber: String = "12345" var body: some View { VStack { CreditCardTextField(number: $cardNumber) .frame(height: 50) .border(.black) } .padding() } }
CreditCardTextField(修改后)
struct CreditCardTextField: UIViewRepresentable { @Binding public var number: String public init(number: Binding<String>) { self._number = number } public func makeUIView(context: Context) -> UITextField { let view = UITextField() view.addTarget(context.coordinator, action: #selector(Coordinator.reformatAsCardNumber), for: .editingChanged) view.delegate = context.coordinator // 初始化时应用格式化 view.text = context.coordinator.formatCardNumber(number) return view } public func updateUIView(_ uiView: UITextField, context: Context) { let formattedNumber = context.coordinator.formatCardNumber(number) // 避免循环更新 if uiView.text != formattedNumber { uiView.text = formattedNumber number = formattedNumber } } public func makeCoordinator() -> Coordinator { Coordinator($number) } // MARK: Coordinator public class Coordinator: NSObject, UITextFieldDelegate { @Binding var number: String private var previousTextFieldContent: String? private var previousSelection: UITextRange? init(_ number: Binding<String>) { self._number = number } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { previousTextFieldContent = textField.text previousSelection = textField.selectedTextRange return true } @objc func reformatAsCardNumber(textField: UITextField, for event: UIControl.Event) { var targetCursorPosition = 0 if let startPosition = textField.selectedTextRange?.start { targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition) } var cardNumberWithoutSpaces = "" if let text = textField.text { cardNumberWithoutSpaces = self.removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition) } if cardNumberWithoutSpaces.count > 16 { textField.text = previousTextFieldContent textField.selectedTextRange = previousSelection return } let cardNumberWithSpaces = self.insertCreditCardSpaces(cardNumberWithoutSpaces, preserveCursorPosition: &targetCursorPosition) textField.text = cardNumberWithSpaces number = cardNumberWithSpaces if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) { textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition) } } func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String { var digitsOnlyString = "" let originalCursorPosition = cursorPosition for i in Swift.stride(from: 0, to: string.count, by: 1) { let characterToAdd = string[string.index(string.startIndex, offsetBy: i)] if characterToAdd >= "0" && characterToAdd <= "9" { digitsOnlyString.append(characterToAdd) } else if i < originalCursorPosition { cursorPosition -= 1 } } return digitsOnlyString } func insertCreditCardSpaces(_ string: String, preserveCursorPosition cursorPosition: inout Int) -> String { var stringWithAddedSpaces = "" let cursorPositionInSpacelessString = cursorPosition for i in 0..<string.count { if i > 0 && (i % 4) == 0 { stringWithAddedSpaces.append(" ") if i < cursorPositionInSpacelessString { cursorPosition += 1 } } let characterToAdd = string[ string.index(string.startIndex, offsetBy: i) ] stringWithAddedSpaces.append(characterToAdd) } return stringWithAddedSpaces } } } // 抽离格式化逻辑到Coordinator扩展 extension CreditCardTextField.Coordinator { func formatCardNumber(_ input: String) -> String { var cursorPosition = 0 let digitsOnly = removeNonDigits(string: input, andPreserveCursorPosition: &cursorPosition) guard digitsOnly.count <= 16 else { return input } return insertCreditCardSpaces(digitsOnly, preserveCursorPosition: &cursorPosition) } }
说明
- 抽离的
formatCardNumber方法统一处理数字过滤和空格插入逻辑,确保初始化、更新、输入时的格式化规则一致。 updateUIView中添加了内容对比,避免因绑定值更新导致的无限循环刷新。- 初始化时直接设置格式化后的文本,保证初始值显示正确。
内容的提问来源于stack exchange,提问作者Arturiusz Pająk
相关产品推荐
相关产品推荐

