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

SwiftUI实现巴西车牌格式自定义键盘技术问询

实现SwiftUI适配巴西Mercosul车牌格式的自定义TextField

针对巴西Mercosul车牌AAA - 0X00的格式要求,结合你提供的代码,以下是完善后的完整实现,解决了自动键盘切换、格式校验、自定义键盘优化等核心问题:

完整代码实现

import CustomKeyboardKit
import SwiftUI
import Combine

class MercosulKeyboardViewModel: ObservableObject {
    // 原始无格式文本(用于逻辑处理)
    @Published var rawText = ""
    // 格式化后的显示文本(带分隔符)
    @Published var formattedText = ""
    @Published var selector = false // false=字母模式,true=数字模式
    @Published var useCustomKeyboard = false // 是否启用自定义键盘

    // 数字键盘布局
    let numberRows = [
        ["1", "2", "3"],
        ["4", "5", "6"],
        ["7", "8", "9"],
        ["OK", "0", "Delete"]
    ]

    // 字母键盘布局(Mercosul车牌后4位仅需A-J字母)
    let alphabeticalRows = [
        ["A", "B", "C"],
        ["D", "E", "F"],
        ["G", "H", "I"],
        ["OK", "J", "Delete"]
    ]

    var currentRows: [[String]] {
        selector ? numberRows : alphabeticalRows
    }

    var onCharacter: ((String) -> Void) = { _ in }
    var onDelete: (() -> Void) = {}
    var onDismiss: (() -> Void) = {}

    init() {
        // 监听原始文本变化,自动处理格式化和键盘切换
        $rawText
            .sink { [weak self] text in
                self?.processText(text)
            }
            .store(in: &cancellables)
    }

    private var cancellables = Set<AnyCancellable>()

    private func processText(_ text: String) {
        // 过滤无效字符,严格遵循车牌规则
        let filteredText = text.uppercased().enumerated().compactMap { index, char -> Character? in
            if index < 3 {
                // 前3位仅允许字母
                char.isLetter ? char : nil
            } else {
                // 后4位:第4位(index=3)数字,第5位(index=4)字母,第6-7位数字
                switch index {
                case 3: char.isNumber ? char : nil
                case 4: char.isLetter ? char : nil
                case 5,6: char.isNumber ? char : nil
                default: nil
                }
            }
        }
        .map { String($0) }
        .joined()

        // 限制总长度(3字母+4字符=7,不含分隔符)
        let trimmedText = String(filteredText.prefix(7))

        // 生成格式化文本
        if trimmedText.count > 3 {
            let prefix = String(trimmedText.prefix(3))
            let suffix = String(trimmedText.suffix(trimmedText.count - 3))
            formattedText = "\(prefix) - \(suffix)"
            useCustomKeyboard = true
        } else {
            formattedText = trimmedText
            useCustomKeyboard = false
        }

        // 更新原始文本为过滤后的值,避免循环触发
        if rawText != trimmedText {
            rawText = trimmedText
        }
    }

    func switchKeyboard() {
        selector.toggle()
    }

    func characterFor(row: Int, column: Int) -> String {
        currentRows[row][column]
    }

    func buttonActionForCharacter(_ character: String) {
        switch character {
        case "Delete":
            deleteAction()
        case "OK":
            dismissAction()
        default:
            guard rawText.count < 7 else { return }
            rawText += character
            onCharacter(character)
        }
    }

    func deleteAction() {
        guard !rawText.isEmpty else { return }
        rawText.removeLast()
        onDelete()
    }

    func dismissAction() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

extension CustomKeyboard {
    static func mercosul(viewModel: MercosulKeyboardViewModel) -> CustomKeyboard {
        CustomKeyboardBuilder { textDocumentProxy, submit, playSystemFeedback in
            viewModel.onCharacter = { character in
                textDocumentProxy.insertText(character)
                playSystemFeedback?()
            }
            viewModel.onDelete = {
                textDocumentProxy.deleteBackward()
                playSystemFeedback?()
            }
            viewModel.onDismiss = { submit?() }

            return VStack(spacing: 12) {
                // 动态切换按钮,显示当前模式的反向选项
                Button(action: viewModel.switchKeyboard) {
                    Text(viewModel.selector ? "切换到字母" : "切换到数字")
                        .font(.subheadline)
                        .foregroundColor(.blue)
                        .padding(.vertical, 8)
                }

                VStack(spacing: 8) {
                    ForEach(0..<viewModel.currentRows.count, id: \.self) { rowIndex in
                        HStack(spacing: 24) {
                            ForEach(0..<viewModel.currentRows[rowIndex].count, id: \.self) { columnIndex in
                                let character = viewModel.characterFor(row: rowIndex, column: columnIndex)
                                Button(action: {
                                    viewModel.buttonActionForCharacter(character)
                                }) {
                                    Text(character)
                                        .font(.title)
                                        .frame(maxWidth: .infinity)
                                        .padding(.vertical, 16)
                                        .background(buttonBackgroundColor(for: character))
                                        .foregroundColor(.white)
                                        .cornerRadius(8)
                                }
                            }
                        }
                    }
                }
                .padding(.horizontal, 16)
                .padding(.bottom, 20)
                .background(Color(.systemGray6))
                .cornerRadius(12)
            }
        }
    }

    private static func buttonBackgroundColor(for character: String) -> Color {
        switch character {
        case "OK": return .green
        case "Delete": return .red
        default: return .gray
        }
    }
}

struct MercosulPlateTextField: View {
    @StateObject private var viewModel = MercosulKeyboardViewModel()

    var body: some View {
        TextField("输入Mercosul车牌", text: $viewModel.formattedText)
            .font(.title)
            .padding()
            .background(Color(.systemGray5))
            .cornerRadius(8)
            .multilineTextAlignment(.center)
            .keyboardType(viewModel.useCustomKeyboard ? .default : .asciiCapable)
            .customKeyboard(viewModel.useCustomKeyboard ? CustomKeyboard.mercosul(viewModel: viewModel) : nil)
            .onChange(of: viewModel.formattedText) { newValue in
                // 将格式化文本转换为原始文本(移除分隔符)
                viewModel.rawText = newValue.replacingOccurrences(of: " - ", with: "")
            }
    }
}

// 预览
struct MercosulPlateTextField_Previews: PreviewProvider {
    static var previews: some View {
        MercosulPlateTextField()
            .padding()
    }
}

核心改进说明

  1. 自动键盘切换

    • 输入前3位时自动使用系统ASCII大写键盘,确保用户输入字母
    • 输入到第4位时自动切换到自定义键盘,适配0X00格式需求
  2. 严格格式校验与自动格式化

    • 前3位仅允许大写字母,自动过滤非字母字符
    • 后4位严格校验:第1位数字、第2位字母、第3-4位数字
    • 输入到第3位时自动插入-分隔符,删除到第3位时自动移除分隔符,保持格式统一
  3. 自定义键盘优化

    • 切换按钮文字动态变化,当前模式一目了然
    • 功能键(OK/Delete)使用不同背景色区分,提升操作辨识度
    • 按钮布局优化,使用弹性宽度适配不同屏幕尺寸
  4. 文本同步逻辑修复

    • 分离原始文本与显示文本,避免格式化操作干扰输入逻辑
    • 确保自定义键盘输入、删除操作与TextField内容完全同步

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 07:56:58