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() } }
核心改进说明
自动键盘切换
- 输入前3位时自动使用系统ASCII大写键盘,确保用户输入字母
- 输入到第4位时自动切换到自定义键盘,适配
0X00格式需求
严格格式校验与自动格式化
- 前3位仅允许大写字母,自动过滤非字母字符
- 后4位严格校验:第1位数字、第2位字母、第3-4位数字
- 输入到第3位时自动插入
-分隔符,删除到第3位时自动移除分隔符,保持格式统一
自定义键盘优化
- 切换按钮文字动态变化,当前模式一目了然
- 功能键(OK/Delete)使用不同背景色区分,提升操作辨识度
- 按钮布局优化,使用弹性宽度适配不同屏幕尺寸
文本同步逻辑修复
- 分离原始文本与显示文本,避免格式化操作干扰输入逻辑
- 确保自定义键盘输入、删除操作与TextField内容完全同步
内容的提问来源于stack exchange,提问作者Ricieri Faviero
相关产品推荐
相关产品推荐

