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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 06:00:10